openvibers/openvibe.ai Commits

Unify provider protocol and search entries in the registry (#432)

OP OpenVibers committed 3 days ago · 37e733b311 · 12 files, +214 −57

* Unify provider protocol and search entries in the existing registry

* Refresh unused provider endpoints without moving configured keys

* Preserve provider setup links and enable test offers explicitly

Changes

ddiff --git a/docs/provider-registry.md b/docs/provider-registry.md
nnew file mode 100644
iindex 0000000..d729e55
---- /dev/null
++++ b/docs/provider-registry.md
@@@ -0,0 +1,54 @@
++# Provider registry
++
++The existing providers table is the authority for provider state, credentials, protocol, priority and capability membership. The seed in server/providers/pool-providers.js and its derived console presets describe defaults; they are not another routing list. An operator can add an OpenAI-compatible endpoint through the existing provider API without writing an adapter.
++
++New provider rows start disabled, including keyless search providers. Existing configured rows keep their status, endpoint, authentication mode and secret reference; legacy catalogue rows without a key are disabled and receive current documented endpoints; keys remain bound to the same origin. Existing model rows, prices, adult policy and routing gates are unchanged. Protocol metadata expands the existing JSON metadata field, so this stage needs no SQL migration. Search adapter identifiers are recorded here; search execution is integrated in the later ordering stage.
++
++Required-provider documentation was checked on 2026-11-10. Meta now documents Model API at api.meta.ai/v1 and explicitly says it does not host Llama; Llama remains available through existing inference vendors. Perplexity uses the Responses-compatible Agent API at /v1. Novita now documents /openai, Together /v1 on api.together.ai, and Hyperbolic /inference/v1 on api.hyperbolic.ai. Existing endpoint bindings are preserved for explicit owner migration. Qwen recommends a workspace-specific Singapore hostname; the legacy international base remains the configurable default. No provider keys or paid calls were used in this verification.
++
++| Provider | API family / adapter | Default base | Models list | Documentation |
++|---|---|---|---|---|
++| Qwen (Alibaba Model Studio) (qwen) | openai / openai | https://dashscope-intl.aliyuncs.com/compatible-mode/v1 | https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models | [Source](https://www.alibabacloud.com/help/en/model-studio/compatibility-of-openai-with-dashscope) |
++| Kimi (Moonshot) (kimi) | openai / openai | https://api.moonshot.ai/v1 | https://api.moonshot.ai/v1/models | [Source](https://platform.moonshot.ai/docs/guide/start-using-kimi-api) |
++| GLM (Z.ai) (glm) | openai / openai | https://api.z.ai/api/paas/v4 | https://api.z.ai/api/paas/v4/models | [Source](https://docs.z.ai/guides/overview/quick-start) |
++| MiniMax (minimax) | openai / openai | https://api.minimax.io/v1 | https://api.minimax.io/v1/models | [Source](https://platform.minimax.io/docs/api-reference/text-openai-api) |
++| DeepInfra (deepinfra) | openai / openai | https://api.deepinfra.com/v1/openai | https://api.deepinfra.com/v1/openai/models | [Source](https://deepinfra.com/docs/openai_api) |
++| Novita AI (novita) | openai / openai | https://api.novita.ai/openai | https://api.novita.ai/openai/models | [Source](https://novita.ai/docs/api-reference/model-apis-llm-list-models) |
++| Featherless AI (featherless) | openai / openai | https://api.featherless.ai/v1 | https://api.featherless.ai/v1/models | [Source](https://api.featherless.ai/v1/models) |
++| Refuseless (the uncensored tier) (refuseless) | openai / openai | https://api.refuseless.com/v1 | https://api.refuseless.com/v1/models | [Source](https://docs.refuseless.com/api/models.md) |
++| Abliteration.ai (the uncensored tier) (abliteration) | openai / openai | https://api.abliteration.ai/v1 | https://api.abliteration.ai/v1/models | [Source](https://docs.abliteration.ai/pricing.md) |
++| OpenRouter (the last fallback) (openrouter) | openai / openai | https://openrouter.ai/api/v1 | https://openrouter.ai/api/v1/models | [Source](https://openrouter.ai/docs/api-reference/overview) |
++| Venice AI (venice) | openai / openai | https://api.venice.ai/api/v1 | https://api.venice.ai/api/v1/models | [Source](https://docs.venice.ai/overview/pricing) |
++| abliteratedmodels.cc (abliteratedmodels) | openai / openai | https://api.abliteratedmodels.cc/v1 | https://api.abliteratedmodels.cc/v1/models | [Source](https://abliteratedmodels.cc/es/) |
++| UplinkSwitch (uplinkswitch) | openai / openai | https://api.uplinkswitch.ai/v1 | https://api.uplinkswitch.ai/v1/models | [Source](https://uplinkswitch.ai/acceptable-use) |
++| Chutes (chutes) | openai / openai | https://llm.chutes.ai/v1 | https://llm.chutes.ai/v1/models | [Source](https://www.chutes.ai/pricing) |
++| SiliconFlow (siliconflow) | openai / openai | https://api.siliconflow.com/v1 | https://api.siliconflow.com/v1/models | [Source](https://docs.siliconflow.com/) |
++| Nebius Token Factory (nebius) | openai / openai | https://api.tokenfactory.nebius.com/v1 | https://api.tokenfactory.nebius.com/v1/models | [Source](https://docs.tokenfactory.nebius.com/quickstart) |
++| Together AI (together) | openai / openai | https://api.together.ai/v1 | https://api.together.ai/v1/models | [Source](https://docs.together.ai/docs/openai-api-compatibility) |
++| Fireworks AI (fireworks) | openai / openai | https://api.fireworks.ai/inference/v1 | https://api.fireworks.ai/inference/v1/models | [Source](https://docs.fireworks.ai/tools-sdks/openai-compatibility) |
++| Groq (groq) | openai / openai | https://api.groq.com/openai/v1 | https://api.groq.com/openai/v1/models | [Source](https://console.groq.com/docs/openai) |
++| Cerebras (cerebras) | openai / openai | https://api.cerebras.ai/v1 | https://api.cerebras.ai/v1/models | [Source](https://inference-docs.cerebras.ai/api-reference/models) |
++| SambaNova (sambanova) | openai / openai | https://api.sambanova.ai/v1 | https://api.sambanova.ai/v1/models | [Source](https://docs-prod.sambanova.ai/docs/api-reference/endpoints/model-list) |
++| Baseten (baseten) | openai / openai | https://inference.baseten.co/v1 | https://inference.baseten.co/v1/models | [Source](https://www.baseten.co/pricing/) |
++| Hyperbolic (hyperbolic) | openai / openai | https://api.hyperbolic.ai/inference/v1 | https://api.hyperbolic.ai/inference/v1/models | [Source](https://docs.hyperbolic.ai/inference/quickstart) |
++| Meta Model API (meta) | openai / openai | https://api.meta.ai/v1 | https://api.meta.ai/v1/models | [Source](https://dev.meta.ai/docs/api-reference) |
++| DeepSeek (deepseek) | openai / openai | https://api.deepseek.com/v1 | https://api.deepseek.com/v1/models | [Source](https://api-docs.deepseek.com/) |
++| Anthropic (anthropic) | anthropic / anthropic | https://api.anthropic.com/v1 | https://api.anthropic.com/v1/models | [Source](https://platform.claude.com/docs/en/api/overview) |
++| OpenAI (openai) | openai / openai | https://api.openai.com/v1 | https://api.openai.com/v1/models | [Source](https://platform.openai.com/docs/api-reference/models/list) |
++| Google Gemini (gemini) | google / openai | https://generativelanguage.googleapis.com/v1beta/openai | https://generativelanguage.googleapis.com/v1beta/openai/models | [Source](https://ai.google.dev/gemini-api/docs/openai) |
++| xAI (xai) | openai / openai | https://api.x.ai/v1 | https://api.x.ai/v1/models | [Source](https://docs.x.ai/docs/api-reference) |
++| Mistral (mistral) | openai / openai | https://api.mistral.ai/v1 | https://api.mistral.ai/v1/models | [Source](https://docs.mistral.ai/api) |
++| Cohere (cohere) | openai / openai | https://api.cohere.com/compatibility/v1 | https://api.cohere.com/v1/models | [Source](https://docs.cohere.com/docs/compatibility-api) |
++| Perplexity (perplexity) | responses / responses | https://api.perplexity.ai/v1 | https://api.perplexity.ai/v1/models | [Source](https://docs.perplexity.ai/docs/agent-api/openai-compatibility) |
++| Brave Search (brave) | adapter / http | https://api.search.brave.com/res/v1 | Not applicable | [Source](https://api-dashboard.search.brave.com/documentation/services/web-search) |
++| Tavily (tavily) | adapter / http | https://api.tavily.com | Not applicable | [Source](https://docs.tavily.com/documentation/api-reference/endpoint/search) |
++| Exa (exa) | adapter / http | https://api.exa.ai | Not applicable | [Source](https://docs.exa.ai/reference/search) |
++| Serper (serper) | adapter / http | https://google.serper.dev | Not applicable | [Source](https://serper.dev/) |
++| Jina (jina) | adapter / http | https://s.jina.ai | Not applicable | [Source](https://jina.ai/reader/) |
++| SearXNG (searxng) | adapter / http | Owner-configured | Not applicable | [Source](https://docs.searxng.org/dev/search_api.html) |
++| Openverse (openverse) | adapter / http | https://api.openverse.org/v1 | Not applicable | [Source](https://api.openverse.org/) |
++| Wikimedia Commons (commons) | adapter / http | https://commons.wikimedia.org/w/api.php | Not applicable | [Source](https://www.mediawiki.org/wiki/API:Search) |
++
++Search capabilities use web-search and image-search, matching the shared placement capability syntax. Brave web and images share one provider/key, as do Serper web and images. Openverse and Wikimedia Commons require no key. SearXNG has no invented public address; the owner supplies their self-hosted base.
++
++For Fable: the existing providers console now receives all seeded provider rows and protocol metadata. Its visual grouping and capability presentation remain for the main session.
ddiff --git a/server/console/presets.js b/server/console/presets.js
iindex dc1d420..b958f93 100644
---- a/server/console/presets.js
++++ b/server/console/presets.js
@@@ -4,20 +4,13 @@
* is an OpenAI-compatible endpoint unless it says otherwise; prices are not here (rate cards are typed in by a person
* from the provider's own price page, with the page and the day, on the model form).
*/
++const { PROVIDERS } = require('../providers/pool-providers');
const PRESETS = [
-- { id: 'deepseek', name: 'DeepSeek', kind: 'openai', base_url: 'https://api.deepseek.com/v1', model: 'deepseek-flash', keys: 'https://platform.deepseek.com/api_keys' },
-- { id: 'openrouter', name: 'OpenRouter', kind: 'openai', base_url: 'https://openrouter.ai/api/v1', model: 'openrouter/auto', keys: 'https://openrouter.ai/settings/keys' },
-- { id: 'groq', name: 'Groq', kind: 'openai', base_url: 'https://api.groq.com/openai/v1', model: 'openai/gpt-oss-20b', keys: 'https://console.groq.com/keys' },
-- { id: 'gemini', name: 'Google Gemini', kind: 'openai', base_url: 'https://generativelanguage.googleapis.com/v1beta/openai', model: '', keys: 'https://aistudio.google.com/apikey' },
-- { id: 'mistral', name: 'Mistral', kind: 'openai', base_url: 'https://api.mistral.ai/v1', model: '', keys: 'https://console.mistral.ai/api-keys' },
-- { id: 'together', name: 'Together AI', kind: 'openai', base_url: 'https://api.together.xyz/v1', model: '', keys: 'https://api.together.ai/settings/api-keys' },
-- { id: 'fireworks', name: 'Fireworks', kind: 'openai', base_url: 'https://api.fireworks.ai/inference/v1', model: '', keys: 'https://fireworks.ai/account/api-keys' },
-- { id: 'cerebras', name: 'Cerebras', kind: 'openai', base_url: 'https://api.cerebras.ai/v1', model: '', keys: 'https://cloud.cerebras.ai/' },
-- { id: 'xai', name: 'xAI', kind: 'openai', base_url: 'https://api.x.ai/v1', model: '', keys: 'https://console.x.ai/' },
-- { id: 'openai', name: 'OpenAI', kind: 'openai', base_url: 'https://api.openai.com/v1', model: 'gpt-5-mini', keys: 'https://platform.openai.com/api-keys' },
-- { id: 'anthropic', name: 'Anthropic', kind: 'anthropic', base_url: 'https://api.anthropic.com/v1', model: '', keys: 'https://console.anthropic.com/settings/keys' },
-- // Anthropic-format endpoints of providers that also speak the OpenAI format (Codes C3): POST /v1/messages reaches
-- // these as they are, an OpenAI-shaped record never is converted.
++ ...PROVIDERS.filter(p => p.capabilities.includes('chat')).map(p => ({
++ id: p.key, name: p.display_name, kind: p.kind, base_url: p.base_url,
++ model: p.credential_default_model ?? p.default_model ?? '', keys: p.keys_url || p.docs_url,
++ })),
++ // Compatibility names for already stored personal credentials; not separate provider rows.
{ id: 'deepseek-anthropic', name: 'DeepSeek (Anthropic format)', kind: 'anthropic', base_url: 'https://api.deepseek.com/anthropic', model: 'deepseek-chat', keys: 'https://platform.deepseek.com/api_keys' },
{ id: 'minimax-anthropic', name: 'MiniMax (Anthropic format)', kind: 'anthropic', base_url: 'https://api.minimax.io/anthropic', model: 'MiniMax-M2', keys: 'https://platform.minimax.io/user-center/basic-information/interface-key' },
{ id: 'custom', name: 'Another OpenAI-compatible API', kind: 'openai', base_url: '', model: '', keys: null },
ddiff --git a/server/providers/pool-providers.js b/server/providers/pool-providers.js
iindex 9128c69..fc9aba7 100644
---- a/server/providers/pool-providers.js
++++ b/server/providers/pool-providers.js
@@@ -47,9 +47,6 @@
const CHECKED = '2026-11-06';
const NEW_CHECKED = '2026-11-09';
--/** The keys of the ten providers, in the order the router should prefer them when the price is equal. */
--const PROVIDER_KEYS = ['qwen', 'kimi', 'glm', 'minimax', 'deepinfra', 'novita', 'featherless', 'refuseless', 'abliteration', 'openrouter', 'venice', 'abliteratedmodels', 'uplinkswitch', 'chutes', 'siliconflow', 'nebius', 'together', 'fireworks', 'groq', 'cerebras', 'sambanova', 'baseten', 'hyperbolic'];
--
/**
* What a provider says it does with a prompt.
*
@@@ -97,6 +94,7 @@ const privacy = (o) => ({
const PROVIDERS = [
{
key: 'qwen',
++ docs_url: 'https://www.alibabacloud.com/help/en/model-studio/compatibility-of-openai-with-dashscope',
display_name: 'Qwen (Alibaba Model Studio)',
base_url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
default_model: 'qwen3.8-max',
@@@ -148,6 +146,7 @@ const PROVIDERS = [
},
{
key: 'kimi',
++ docs_url: 'https://platform.moonshot.ai/docs/guide/start-using-kimi-api',
display_name: 'Kimi (Moonshot)',
base_url: 'https://api.moonshot.ai/v1',
default_model: 'kimi-k3.1-pro',
@@@ -198,6 +197,7 @@ const PROVIDERS = [
},
{
key: 'glm',
++ docs_url: 'https://docs.z.ai/guides/overview/quick-start',
display_name: 'GLM (Z.ai)',
base_url: 'https://api.z.ai/api/paas/v4',
default_model: 'glm-5.3-flash',
@@@ -258,6 +258,7 @@ const PROVIDERS = [
},
{
key: 'minimax',
++ docs_url: 'https://platform.minimax.io/docs/api-reference/text-openai-api',
display_name: 'MiniMax',
base_url: 'https://api.minimax.io/v1',
default_model: 'MiniMax-M3.1-Flash',
@@@ -296,6 +297,7 @@ const PROVIDERS = [
},
{
key: 'deepinfra',
++ docs_url: 'https://deepinfra.com/docs/openai_api',
display_name: 'DeepInfra',
base_url: 'https://api.deepinfra.com/v1/openai',
default_model: 'deepseek-ai/DeepSeek-V4.1-Flash-1017',
@@@ -369,8 +371,9 @@ const PROVIDERS = [
},
{
key: 'novita',
++ docs_url: 'https://novita.ai/docs/api-reference/model-apis-llm-list-models',
display_name: 'Novita AI',
-- base_url: 'https://api.novita.ai/v3/openai',
++ base_url: 'https://api.novita.ai/openai',
default_model: 'deepseek/deepseek-v4.1-flash-1017',
timeout_ms: 90000,
capabilities: ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'json', 'tools'],
@@@ -378,13 +381,13 @@ const PROVIDERS = [
priority: 30,
privacy: privacy({
label: 'anonymized',
-- terms_url: 'https://api.novita.ai/v3/openai/models',
++ terms_url: 'https://api.novita.ai/openai/models',
wording: 'Novita has no readable privacy policy page (the address returns a not-found page on 2026-11-06), so nothing says zero retention or no training',
}),
data_policy: policy({
trains: null, retention: null, region: null,
retention_note: 'not stated (the provider has no readable privacy page)',
-- terms: 'https://api.novita.ai/v3/openai/models',
++ terms: 'https://api.novita.ai/openai/models',
resale: 'UNVERIFIED (2026-11-03 matrix): the terms page returned 404',
}),
@@@ -392,7 +395,7 @@ const PROVIDERS = [
{
model_key: 'deepseek/deepseek-v4.1-flash-1017', display_name: 'DeepSeek V4.1 Flash 1017', family: 'deepseek', context_window: 1048576,
cost: { in_per_mtok: 0.30, cached_per_mtok: 0.006, out_per_mtok: 1.20 },
-- source: 'https://api.novita.ai/v3/openai/models', checked: CHECKED,
++ source: 'https://api.novita.ai/openai/models', checked: CHECKED,
supports: { json: true, tools: true, streaming: true, vision: false },
quantization: null, refusal_rate: null,
note: 'the list marks the model as serverless with function calling, structured outputs and reasoning',
@@@ -400,28 +403,28 @@ const PROVIDERS = [
{
model_key: 'qwen/qwen3.8-max', display_name: 'Qwen3.8-Max', family: 'qwen', context_window: 1000000,
cost: { in_per_mtok: 2.00, cached_per_mtok: 0.25, out_per_mtok: 6.00 },
-- source: 'https://api.novita.ai/v3/openai/models', checked: CHECKED,
++ source: 'https://api.novita.ai/openai/models', checked: CHECKED,
supports: { json: true, tools: true, streaming: true, vision: false },
quantization: null, refusal_rate: null,
},
{
model_key: 'zai-org/glm-5.3', display_name: 'GLM-5.3', family: 'glm', context_window: 1048576,
cost: { in_per_mtok: 1.40, cached_per_mtok: 0.26, out_per_mtok: 4.40 },
-- source: 'https://api.novita.ai/v3/openai/models', checked: CHECKED,
++ source: 'https://api.novita.ai/openai/models', checked: CHECKED,
supports: { json: true, tools: true, streaming: true, vision: false },
quantization: null, refusal_rate: null,
},
{
model_key: 'moonshotai/kimi-k3.1-pro', display_name: 'Kimi K3.1 Pro', family: 'kimi', context_window: 1048576,
cost: { in_per_mtok: 2.00, cached_per_mtok: 0.20, out_per_mtok: 10.00 },
-- source: 'https://api.novita.ai/v3/openai/models', checked: CHECKED,
++ source: 'https://api.novita.ai/openai/models', checked: CHECKED,
supports: { json: true, tools: true, streaming: true, vision: false },
quantization: null, refusal_rate: null,
},
{
model_key: 'xiaomimimo/mimo-v2.6-flash', display_name: 'MiMo V2.6 Flash', family: 'mimo', context_window: 1048576,
cost: { in_per_mtok: 0.14, cached_per_mtok: 0.0028, out_per_mtok: 0.28 },
-- source: 'https://api.novita.ai/v3/openai/models', checked: CHECKED,
++ source: 'https://api.novita.ai/openai/models', checked: CHECKED,
supports: { json: true, tools: true, streaming: true, vision: false },
quantization: null, refusal_rate: null,
},
@@@ -612,7 +615,8 @@ const PROVIDERS = [
],
},
{
-- key: 'openrouter',
++ key: 'openrouter', keys_url: 'https://openrouter.ai/settings/keys',
++ docs_url: 'https://openrouter.ai/docs/api-reference/overview',
display_name: 'OpenRouter (the last fallback)',
base_url: 'https://openrouter.ai/api/v1',
default_model: 'openrouter/auto',
@@@ -642,18 +646,18 @@ const PROVIDERS = [
['uplinkswitch', 'UplinkSwitch', 'https://api.uplinkswitch.ai/v1', 'abliterated', 'https://uplinkswitch.ai/acceptable-use', 'UNKNOWN', 'UNVERIFIED', { in: null, cached: null, out: null }, null, true],
['chutes', 'Chutes', 'https://llm.chutes.ai/v1', 'Qwen/Qwen3-32B-TEE', 'https://www.chutes.ai/pricing', 'UNKNOWN', 'UNVERIFIED', { in: 0.104, cached: null, out: 0.416 }, 40960, false],
['siliconflow', 'SiliconFlow', 'https://api.siliconflow.com/v1', 'Qwen/Qwen3-32B', 'https://docs.siliconflow.com/', 'UNKNOWN', 'UNVERIFIED', { in: null, cached: null, out: null }, null, false],
-- ['nebius', 'Nebius Token Factory', 'https://api.tokenfactory.nebius.com/v1', 'Qwen/Qwen3-30B-A3B-Instruct-2507', 'https://nebius.com/services/token-factory/models/qwen-models-inference', 'UNKNOWN', 'UNVERIFIED', { in: 0.10, cached: null, out: 0.30 }, 262144, false],
-- ['together', 'Together AI', 'https://api.together.xyz/v1', 'openai/gpt-oss-120b', 'https://www.together.ai/pricing', 'UNKNOWN', 'RESTRICTED', { in: 0.15, cached: null, out: 0.60 }, 131072, false],
-- ['fireworks', 'Fireworks AI', 'https://api.fireworks.ai/inference/v1', 'accounts/fireworks/models/gpt-oss-120b', 'https://fireworks.ai/models', 'UNKNOWN', 'UNVERIFIED', { in: 0.15, cached: null, out: 0.60 }, 131072, false],
-- ['groq', 'Groq', 'https://api.groq.com/openai/v1', 'openai/gpt-oss-120b', 'https://console.groq.com/docs/model/openai/gpt-oss-120b', 'UNKNOWN', 'UNVERIFIED', { in: 0.15, cached: 0.075, out: 0.60 }, 131072, false],
-- ['cerebras', 'Cerebras', 'https://api.cerebras.ai/v1', 'gpt-oss-120b', 'https://api.cerebras.ai/public/v1/models', 'UNKNOWN', 'UNVERIFIED', { in: null, cached: null, out: null }, 131072, false],
-- ['sambanova', 'SambaNova', 'https://api.sambanova.ai/v1', 'gpt-oss-120b', 'https://cloud.sambanova.ai/plans/pricing', 'UNKNOWN', 'UNVERIFIED', { in: 0.22, cached: null, out: 0.59 }, 131072, false],
++ ['nebius', 'Nebius Token Factory', 'https://api.tokenfactory.nebius.com/v1', 'Qwen/Qwen3-30B-A3B-Instruct-2507', 'https://nebius.com/services/token-factory/models/qwen-models-inference', 'UNKNOWN', 'UNVERIFIED', { in: 0.10, cached: null, out: 0.30 }, 262144, false, 'https://docs.tokenfactory.nebius.com/quickstart'],
++ ['together', 'Together AI', 'https://api.together.ai/v1', 'openai/gpt-oss-120b', 'https://www.together.ai/pricing', 'UNKNOWN', 'RESTRICTED', { in: 0.15, cached: null, out: 0.60 }, 131072, false, 'https://docs.together.ai/docs/openai-api-compatibility', 'https://api.together.ai/settings/api-keys', ''],
++ ['fireworks', 'Fireworks AI', 'https://api.fireworks.ai/inference/v1', 'accounts/fireworks/models/gpt-oss-120b', 'https://fireworks.ai/models', 'UNKNOWN', 'UNVERIFIED', { in: 0.15, cached: null, out: 0.60 }, 131072, false, 'https://docs.fireworks.ai/tools-sdks/openai-compatibility', 'https://fireworks.ai/account/api-keys', ''],
++ ['groq', 'Groq', 'https://api.groq.com/openai/v1', 'openai/gpt-oss-120b', 'https://console.groq.com/docs/model/openai/gpt-oss-120b', 'UNKNOWN', 'UNVERIFIED', { in: 0.15, cached: 0.075, out: 0.60 }, 131072, false, 'https://console.groq.com/docs/openai', 'https://console.groq.com/keys', 'openai/gpt-oss-20b'],
++ ['cerebras', 'Cerebras', 'https://api.cerebras.ai/v1', 'gpt-oss-120b', 'https://api.cerebras.ai/public/v1/models', 'UNKNOWN', 'UNVERIFIED', { in: null, cached: null, out: null }, 131072, false, 'https://inference-docs.cerebras.ai/api-reference/models', 'https://cloud.cerebras.ai/', ''],
++ ['sambanova', 'SambaNova', 'https://api.sambanova.ai/v1', 'gpt-oss-120b', 'https://cloud.sambanova.ai/plans/pricing', 'UNKNOWN', 'UNVERIFIED', { in: 0.22, cached: null, out: 0.59 }, 131072, false, 'https://docs-prod.sambanova.ai/docs/api-reference/endpoints/model-list'],
['baseten', 'Baseten', 'https://inference.baseten.co/v1', 'openai/gpt-oss-120b', 'https://www.baseten.co/pricing/', 'UNKNOWN', 'UNVERIFIED', { in: null, cached: null, out: null }, 131072, false],
-- ['hyperbolic', 'Hyperbolic', 'https://api.hyperbolic.xyz/v1', 'openai/gpt-oss-120b', 'https://docs.hyperbolic.xyz/docs/hyperbolic-ai-inference-pricing', 'UNKNOWN', 'UNVERIFIED', { in: null, cached: null, out: null }, 131072, false],
-- ].map(([key, display_name, base_url, model_key, source, privacyLabel, provider_access, prices, context_window, uncensored]) => {
++ ['hyperbolic', 'Hyperbolic', 'https://api.hyperbolic.ai/inference/v1', 'openai/gpt-oss-120b', 'https://docs.hyperbolic.xyz/docs/hyperbolic-ai-inference-pricing', 'UNKNOWN', 'UNVERIFIED', { in: null, cached: null, out: null }, 131072, false, 'https://docs.hyperbolic.ai/inference/quickstart'],
++ ].map(([key, display_name, base_url, model_key, source, privacyLabel, provider_access, prices, context_window, uncensored, docs_url, keys_url, credential_default_model]) => {
const privateTier = privacyLabel === 'PRIVATE';
return {
-- key, display_name, base_url, default_model: model_key, timeout_ms: 60000,
++ key, display_name, base_url, docs_url, keys_url, credential_default_model, default_model: model_key, timeout_ms: 60000,
capabilities: ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'json', 'tools'],
provider_access, priority: 40, ...(uncensored ? { uncensored: true } : {}),
privacy: privacy({ label: privateTier ? 'private' : 'anonymized', terms_url: privateTier ? source : null,
@@@ -672,8 +676,62 @@ const PROVIDERS = [
],
};
}),
++ // Providers without a hand-kept model list: the owner syncs and enables their models.
++ { key: 'meta', display_name: 'Meta Model API', base_url: 'https://api.meta.ai/v1',
++ docs_url: 'https://dev.meta.ai/docs/api-reference', capabilities: ['chat', 'vision', 'tools'] },
++ { key: 'deepseek', keys_url: 'https://platform.deepseek.com/api_keys', display_name: 'DeepSeek', base_url: 'https://api.deepseek.com/v1', default_model: 'deepseek-flash',
++ docs_url: 'https://api-docs.deepseek.com/', capabilities: ['chat', 'vision', 'tools', 'web-search'], search_adapters: ['deepseek'] },
++ { key: 'anthropic', keys_url: 'https://console.anthropic.com/settings/keys', display_name: 'Anthropic', kind: 'anthropic', base_url: 'https://api.anthropic.com/v1', auth_mode: 'x-api-key',
++ docs_url: 'https://platform.claude.com/docs/en/api/overview', capabilities: ['chat', 'messages', 'vision', 'tools'] },
++ { key: 'openai', keys_url: 'https://platform.openai.com/api-keys', display_name: 'OpenAI', base_url: 'https://api.openai.com/v1', default_model: 'gpt-5-mini',
++ docs_url: 'https://platform.openai.com/docs/api-reference/models/list', capabilities: ['chat', 'vision', 'tools', 'image', 'video', 'speech', 'transcribe', 'embed'] },
++ { key: 'gemini', keys_url: 'https://aistudio.google.com/apikey', display_name: 'Google Gemini', base_url: 'https://generativelanguage.googleapis.com/v1beta/openai',
++ api_family: 'google', docs_url: 'https://ai.google.dev/gemini-api/docs/openai', capabilities: ['chat', 'vision', 'tools', 'embed'] },
++ { key: 'xai', keys_url: 'https://console.x.ai/', display_name: 'xAI', base_url: 'https://api.x.ai/v1',
++ docs_url: 'https://docs.x.ai/docs/api-reference', capabilities: ['chat', 'vision', 'tools', 'image', 'video'] },
++ { key: 'mistral', keys_url: 'https://console.mistral.ai/api-keys', display_name: 'Mistral', base_url: 'https://api.mistral.ai/v1',
++ docs_url: 'https://docs.mistral.ai/api', capabilities: ['chat', 'vision', 'tools', 'embed', 'transcribe'] },
++ { key: 'cohere', display_name: 'Cohere', base_url: 'https://api.cohere.com/compatibility/v1', models_endpoint: 'https://api.cohere.com/v1/models',
++ docs_url: 'https://docs.cohere.com/docs/compatibility-api', capabilities: ['chat', 'tools', 'embed', 'rerank'] },
++ { key: 'perplexity', display_name: 'Perplexity', kind: 'responses', base_url: 'https://api.perplexity.ai/v1',
++ docs_url: 'https://docs.perplexity.ai/docs/agent-api/openai-compatibility', capabilities: ['chat', 'web-search'], search_adapters: ['perplexity'] },
++ { key: 'brave', display_name: 'Brave Search', kind: 'http', base_url: 'https://api.search.brave.com/res/v1',
++ auth_header: 'x-subscription-token', models_endpoint: null, capabilities: ['web-search', 'image-search'],
++ search_adapters: ['brave', 'brave-images'], docs_url: 'https://api-dashboard.search.brave.com/documentation/services/web-search' },
++ { key: 'tavily', display_name: 'Tavily', kind: 'http', base_url: 'https://api.tavily.com', models_endpoint: null,
++ capabilities: ['web-search'], search_adapters: ['tavily'], docs_url: 'https://docs.tavily.com/documentation/api-reference/endpoint/search' },
++ { key: 'exa', display_name: 'Exa', kind: 'http', base_url: 'https://api.exa.ai', auth_mode: 'x-api-key', models_endpoint: null,
++ capabilities: ['web-search'], search_adapters: ['exa'], docs_url: 'https://docs.exa.ai/reference/search' },
++ { key: 'serper', display_name: 'Serper', kind: 'http', base_url: 'https://google.serper.dev', auth_mode: 'x-api-key', models_endpoint: null,
++ capabilities: ['web-search', 'image-search'], search_adapters: ['serper', 'serper-images'], docs_url: 'https://serper.dev/' },
++ { key: 'jina', display_name: 'Jina', kind: 'http', base_url: 'https://s.jina.ai', models_endpoint: null,
++ capabilities: ['web-search'], search_adapters: ['jina'], docs_url: 'https://jina.ai/reader/' },
++ { key: 'searxng', display_name: 'SearXNG', kind: 'http', base_url: null, auth_mode: 'none', models_endpoint: null,
++ capabilities: ['web-search'], search_adapters: ['searxng'], docs_url: 'https://docs.searxng.org/dev/search_api.html' },
++ { key: 'openverse', display_name: 'Openverse', kind: 'http', base_url: 'https://api.openverse.org/v1', auth_mode: 'none', models_endpoint: null,
++ capabilities: ['image-search'], search_adapters: ['openverse'], docs_url: 'https://api.openverse.org/' },
++ { key: 'commons', display_name: 'Wikimedia Commons', kind: 'http', base_url: 'https://commons.wikimedia.org/w/api.php', auth_mode: 'none', models_endpoint: null,
++ capabilities: ['image-search'], search_adapters: ['commons'], docs_url: 'https://www.mediawiki.org/wiki/API:Search' },
++
];
++// The seed, console and personal-key forms consume these same records.
++for (const p of PROVIDERS) {
++ if (p.capabilities.includes('chat')) p.capabilities = [...new Set([...p.capabilities, 'generate', 'summarize', 'classify', 'extract', 'enrich', 'json'])];
++ p.kind ||= 'openai';
++ p.auth_mode ||= 'bearer';
++ p.api_family ||= p.kind === 'http' ? 'adapter' : p.kind;
++ if (p.models_endpoint === undefined) p.models_endpoint = p.base_url ? `${p.base_url}/models` : null;
++ p.models ||= [];
++ p.provider_access ||= 'UNVERIFIED';
++ p.priority ??= 100;
++ p.timeout_ms ||= 30000;
++ p.docs_url ||= p.models[0]?.source || null;
++ p.data_policy ||= policy({ checked: '2026-11-10' });
++ p.privacy ||= privacy({ label: 'anonymized', wording: 'Data retention and training terms have not been verified.', checked: '2026-11-10' });
++}
++const PROVIDER_KEYS = PROVIDERS.map(p => p.key);
++
const byKey = new Map(PROVIDERS.map((p) => [p.key, p]));
/** The name a provider's model is known by, without its namespace: `deepseek/deepseek-chat` -> `deepseek-chat`. */
ddiff --git a/server/providers/pool-seed.js b/server/providers/pool-seed.js
iindex 6b7cb74..8ba9d23 100644
---- a/server/providers/pool-seed.js
++++ b/server/providers/pool-seed.js
@@@ -17,16 +17,20 @@
*/
const pool = require('./pool-providers');
++const { providerSecret } = require('../util');
/** The registry row of one catalogued provider. */
function recordOf(p) {
return {
-- key: p.key, display_name: p.display_name, kind: 'openai', status: 'active',
-- base_url: p.base_url, auth_mode: 'bearer', secret_ref: `vault:${p.key}`,
++ key: p.key, display_name: p.display_name, kind: p.kind, status: 'disabled',
++ base_url: p.base_url, auth_mode: p.auth_mode, secret_ref: p.auth_mode === 'none' ? null : `vault:${p.key}`,
default_model: p.default_model || null, capabilities: p.capabilities, timeout_ms: p.timeout_ms,
priority: p.priority, billing_profile: 'metered', provider_access: p.provider_access,
metadata: {
catalog: 'pool-providers',
++ api_family: p.api_family, models_endpoint: p.models_endpoint,
++ auth_header: p.auth_header || null, docs_url: p.docs_url || null,
++ search_adapters: p.search_adapters || [],
data_policy: p.data_policy,
// The privacy label of the provider: `private` only where its own terms say it keeps nothing and trains on
// nothing, `anonymized` for everyone else. The wording and the page it was read from travel with it.
@@@ -83,7 +87,7 @@ function cataloguePrice(row) {
*
* → { providers: [key], models: [key], skipped: [key] }
*/
--async function seedPoolProviders({ registry, actor = 'system' }) {
++async function seedPoolProviders({ registry, actor = 'system', env = process.env }) {
const out = { providers: [], models: [], skipped: [] };
for (const p of pool.PROVIDERS) {
const prev = await registry.getProvider(p.key);
@@@ -91,16 +95,30 @@ async function seedPoolProviders({ registry, actor = 'system' }) {
await registry.upsertProvider(recordOf(p), { actor, origin: 'catalog' });
out.providers.push(p.key);
} else if (prev.origin === 'catalog') {
-- // The catalogue owns this row: keep the owner's status and key slot, refresh the rest.
++ const defaults = recordOf(p);
++ const configured = prev.auth_mode === 'none' || Boolean(providerSecret(prev, env));
++ if (configured && prev.base_url !== p.base_url) defaults.metadata.models_endpoint = prev.base_url ? `${prev.base_url.replace(/\/+$/, '')}/models` : null;
++ const metadata = { ...defaults.metadata, ...prev.metadata };
++ if (!configured && prev.base_url !== p.base_url) {
++ metadata.models_endpoint = defaults.metadata.models_endpoint;
++ metadata.api_family = defaults.metadata.api_family;
++ }
++ // A key is bound to its origin. A documentation refresh must never move a working
++ // provider or change its wire format; endpoint changes are explicit owner edits.
await registry.upsertProvider({
-- ...recordOf(p),
-- status: prev.status,
-- secret_ref: prev.secret_ref || `vault:${p.key}`,
++ ...defaults,
++ ...(configured ? { base_url: prev.base_url, kind: prev.kind, auth_mode: prev.auth_mode } : {}),
++ status: prev.status === 'active' && !configured ? 'disabled' : prev.status,
++ secret_ref: prev.secret_ref,
provider_access: prev.provider_access || p.provider_access,
-- metadata: { ...prev.metadata, ...recordOf(p).metadata },
++ metadata,
}, { actor, origin: 'catalog' });
} else {
-- out.skipped.push(p.key); // the owner's own provider: never touched
++ out.skipped.push(p.key);
++ const protocol = recordOf(p).metadata;
++ if (prev.base_url !== p.base_url) protocol.models_endpoint = prev.base_url ? `${prev.base_url.replace(/\/+$/, '')}/models` : null;
++ const additions = Object.fromEntries(Object.entries(protocol).filter(([k]) => ['api_family', 'models_endpoint', 'auth_header', 'docs_url', 'search_adapters'].includes(k) && prev.metadata[k] === undefined));
++ if (Object.keys(additions).length) await registry.upsertProvider({ key: p.key, metadata: { ...prev.metadata, ...additions } }, { actor, origin: prev.origin });
}
for (const m of p.models) {
const row = await registry.getModel(p.key, m.model_key);
ddiff --git a/server/registry.js b/server/registry.js
iindex 7e3c7fa..1a2e42a 100644
---- a/server/registry.js
++++ b/server/registry.js
@@@ -17,7 +17,7 @@ const PROVIDER_KINDS = ['stub', 'openai', 'responses', 'anthropic', 'http', 'whi
const BILLING_PROFILES = ['metered', 'subscription', 'payg', 'free', 'byok'];
// AI Exchange D3: who may resell this offer's capacity. Defaults to UNVERIFIED (migrations/0024).
const PROVIDER_ACCESS = ['OWN_DEPLOYMENT', 'AUTHORIZED_WHOLESALE', 'CUSTOMER_BYOK', 'RESTRICTED', 'UNVERIFIED'];
--const FEATURES = ['chat', 'complete', 'messages', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'embed', 'vision', 'json', 'tools', 'transcribe', 'image'];
++const FEATURES = ['chat', 'complete', 'messages', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'embed', 'vision', 'json', 'tools', 'transcribe', 'image', 'video', 'speech', 'rerank', 'web-search', 'image-search'];
const LIFECYCLE = ['draft', 'active', 'deprecated', 'archived'];
const KEY_RE = /^[a-z][a-z0-9_-]*(\.[a-z0-9_-]+)*$/;
const flag = (v) => v === true || v === 1 || /^(1|true|yes|on)$/i.test(String(v == null ? '' : v));
ddiff --git a/server/workflows/seed.js b/server/workflows/seed.js
iindex d6f982c..88ec261 100644
---- a/server/workflows/seed.js
++++ b/server/workflows/seed.js
@@@ -147,7 +147,7 @@ async function seed({ registry, quotas, config, env = process.env, db }) {
// DeepInfra, Novita, Featherless, Refuseless, Abliteration.ai and OpenRouter. Each is registered off until its key
// is in the owner settings; the pool skips one whose key does not resolve. Prices come from each provider's own
// page, with that page and the day they were read (server/providers/pool-providers.js).
-- await poolSeed.seedPoolProviders({ registry, actor: 'seed' });
++ await poolSeed.seedPoolProviders({ registry, actor: 'seed', env });
const shared = await registry.getProvider('shared');
// Models
ddiff --git a/test/ai-provider-adapters.test.js b/test/ai-provider-adapters.test.js
iindex 6853e07..4582eda 100644
---- a/test/ai-provider-adapters.test.js
++++ b/test/ai-provider-adapters.test.js
@@@ -44,11 +44,20 @@ const iso = (n) => new Date(n).toISOString();
const CHAT = { messages: [{ role: 'user', content: 'x' }], timeoutMs: 3000 };
t.test('provider presets are OpenAI-compatible, with a base URL, a timeout, an access mode and a privacy label', () => {
-- assert.deepStrictEqual(pool.PROVIDERS.map((p) => p.key),
++ assert.deepStrictEqual(pool.PROVIDERS.slice(0, 23).map((p) => p.key),
['qwen', 'kimi', 'glm', 'minimax', 'deepinfra', 'novita', 'featherless', 'refuseless', 'abliteration', 'openrouter', 'venice', 'abliteratedmodels', 'uplinkswitch', 'chutes', 'siliconflow', 'nebius', 'together', 'fireworks', 'groq', 'cerebras', 'sambanova', 'baseten', 'hyperbolic']);
assert.deepStrictEqual(pool.PROVIDER_KEYS, pool.PROVIDERS.map((p) => p.key));
++ assert.strictEqual(new Set(pool.PROVIDER_KEYS).size, 40);
++ assert.strictEqual(pool.byKey.get('meta').base_url, 'https://api.meta.ai/v1');
++ assert.strictEqual(pool.byKey.get('perplexity').kind, 'responses');
++ assert.strictEqual(require('../server/console/presets').credentialProvider('groq').model, 'openai/gpt-oss-20b', 'existing personal-key default is preserved');
++ assert.deepStrictEqual(pool.byKey.get('brave').search_adapters, ['brave', 'brave-images']);
++ assert.strictEqual(pool.byKey.get('openverse').auth_mode, 'none');
++ assert.strictEqual(pool.byKey.get('commons').auth_mode, 'none');
for (const p of pool.PROVIDERS) {
-- assert.ok(/^https:\/\//.test(p.base_url), `${p.key} base_url`);
++ assert.ok(p.key === 'searxng' && p.base_url === null || /^https:\/\//.test(p.base_url), `${p.key} base_url`);
++ assert.strictEqual(seed.recordOf(p).status, 'disabled', `${p.key} starts off`);
++ assert.ok(p.api_family && Object.hasOwn(p, 'models_endpoint'), `${p.key} has protocol metadata`);
assert.ok(p.timeout_ms >= 1000, `${p.key} timeout`);
assert.ok(['OWN_DEPLOYMENT', 'AUTHORIZED_WHOLESALE', 'CUSTOMER_BYOK', 'RESTRICTED', 'UNVERIFIED'].includes(p.provider_access), p.key);
assert.ok(p.data_policy, `${p.key} data policy`);
@@@ -142,8 +151,32 @@ t.test('the seed writes the privacy label, the quantization and the refusal rate
assert.strictEqual(saved.get('qwen').base_url, 'https://proxy.example/v1');
});
++t.test('seed refreshes unused endpoints while preserving configured and owner-managed bindings', async () => {
++ const source = pool.byKey.get('hyperbolic');
++ const records = new Map([['hyperbolic', {
++ ...seed.recordOf(source), status: 'active', origin: 'catalog',
++ base_url: 'https://legacy.example/v1', secret_ref: 'env:REGISTRY_TEST_KEY', metadata: {},
++ }]]);
++ const registry = {
++ getProvider: async key => records.get(key) || null,
++ upsertProvider: async (row, options) => records.set(row.key, { ...records.get(row.key), ...row, origin: options.origin }),
++ getModel: async () => null, upsertModel: async () => {},
++ };
++ await seed.seedPoolProviders({ registry, env: { REGISTRY_TEST_KEY: 'fixture-provider-credential' } });
++ assert.strictEqual(records.get('hyperbolic').base_url, 'https://legacy.example/v1');
++ assert.strictEqual(records.get('hyperbolic').status, 'active');
++ await seed.seedPoolProviders({ registry, env: {} });
++ assert.strictEqual(records.get('hyperbolic').base_url, source.base_url);
++ assert.strictEqual(records.get('hyperbolic').metadata.models_endpoint, source.models_endpoint);
++ assert.strictEqual(records.get('hyperbolic').status, 'disabled');
++ records.set('hyperbolic', { ...records.get('hyperbolic'), origin: 'admin', base_url: 'https://owner.example/v1', status: 'active' });
++ await seed.seedPoolProviders({ registry, env: {} });
++ assert.strictEqual(records.get('hyperbolic').base_url, 'https://owner.example/v1');
++ assert.strictEqual(records.get('hyperbolic').status, 'active');
++});
++
t.test('the recorded shape of every provider is parsed by the OpenAI adapter, tool calls included', async () => {
-- for (const key of pool.PROVIDER_KEYS) {
++ for (const key of pool.PROVIDERS.filter(p => p.kind === 'openai').map(p => p.key)) {
const recorded = fixtures[key] || fixtures.openrouter;
assert.ok(recorded && recorded.completion, `${key} fixture`);
const srv = await seamServer(() => recorded.completion);
@@@ -165,7 +198,7 @@ t.test('the recorded shape of every provider is parsed by the OpenAI adapter, to
t.test('a catalogue offer with no key is off: the pool skips it no_credentials', async () => {
// The commercial gate (an UNVERIFIED offer) keeps most of the catalogue out of the managed pool; the
// credential gate is what this test is about, so the row carries the access mode the owner would grant.
-- const row = { ...seed.recordOf(pool.byKey.get('qwen')), provider_access: 'OWN_DEPLOYMENT', origin: 'catalog' };
++ const row = { ...seed.recordOf(pool.byKey.get('qwen')), status: 'active', provider_access: 'OWN_DEPLOYMENT', origin: 'catalog' };
const registry = {
getProvider: async (k) => (k === 'qwen' ? row : null),
listProviders: async () => [row],
@@@ -200,14 +233,15 @@ t.test('a provider that says the account has no money left is exhaustion, and it
assert.strictEqual(insufficientBalance(new ProviderError('the backend fell over', { status: 500, code: 'provider.http', body: {} })), false);
});
--t.test('a booted service seeds the ten offers, each with its own key slot', async () => {
++t.test('a booted service seeds the registry and preserves the existing environment key slot', async () => {
const h = await boot({ env: { AI_STUB_FALLBACK: 'false' } });
try {
const rows = await h.registry.listProviders();
for (const p of pool.PROVIDERS) {
const row = rows.find((x) => x.key === p.key);
assert.ok(row, `${p.key} seeded`);
-- assert.strictEqual(row.secret_ref, `vault:${p.key}`, `${p.key} key slot`);
++ assert.strictEqual(row.secret_ref, p.key === 'deepseek' ? 'env:DEEPSEEK_API_KEY' : p.auth_mode === 'none' ? null : `vault:${p.key}`, `${p.key} key slot`);
++ if (p.key === 'deepseek') continue; // the existing environment-backed seed retains its configuration
assert.ok(row.metadata && row.metadata.catalog === 'pool-providers', `${p.key} catalogue row`);
assert.strictEqual(row.metadata.privacy.label, p.privacy.label, `${p.key} privacy label`);
}
@@@ -215,7 +249,7 @@ t.test('a booted service seeds the ten offers, each with its own key slot', asyn
const route = { key: 'test.seeded', primary: { provider: 'qwen', model: null }, fallbacks: [], timeout_ms: 3000 };
const { explain } = await h.pool.candidates(route, ['chat'], 'chat', {});
assert.strictEqual(explain.candidates[0].eligible, false, 'an offer with no key is not eligible');
-- assert.ok(String(explain.candidates[0].excluded_reason).includes('no_credentials'), explain.candidates[0].excluded_reason);
++ assert.ok(/no_credentials|disabled/.test(String(explain.candidates[0].excluded_reason)), explain.candidates[0].excluded_reason);
await assert.rejects(
h.pool.execute(route, 'chat', { messages: [{ role: 'user', content: 'x' }], timeoutMs: 3000 }, { routeKey: route.key }),
(err) => err.code === 'provider.unavailable',
ddiff --git a/test/ai-uncensored-policy.test.js b/test/ai-uncensored-policy.test.js
iindex 825421d..5fec833 100644
---- a/test/ai-uncensored-policy.test.js
++++ b/test/ai-uncensored-policy.test.js
@@@ -199,7 +199,7 @@ t.test('a booted service seeds the catalogue off, and the uncensored request is
for (const p of pool.PROVIDERS) {
const row = seeded.find((x) => x.key === p.key);
assert.ok(row, `${p.key} seeded`);
-- assert.strictEqual(row.secret_ref, `vault:${p.key}`, `${p.key} key slot`);
++ assert.strictEqual(row.secret_ref, p.key === 'deepseek' ? 'env:DEEPSEEK_API_KEY' : p.auth_mode === 'none' ? null : `vault:${p.key}`, `${p.key} key slot`);
assert.ok(await h.registry.getModel(p.key, p.models[0] && p.models[0].model_key || '').catch(() => null) || p.models.length === 0, `${p.key} model`);
}
await h.registry.upsertProvider({ key: 'refuseless', display_name: 'Refuseless', kind: 'openai', status: 'active', base_url: seam.url, auth_mode: 'none', secret_ref: null, default_model: 'deepseek-v4.1-flash-abliterated', capabilities: ['chat', 'generate', 'complete'], timeout_ms: 5000, priority: 1, provider_access: 'AUTHORIZED_WHOLESALE', metadata: { uncensored: true } }, { origin: 'admin' });
ddiff --git a/test/exchange-presets-receipts.test.js b/test/exchange-presets-receipts.test.js
iindex fc5704b..9468747 100644
---- a/test/exchange-presets-receipts.test.js
++++ b/test/exchange-presets-receipts.test.js
@@@ -66,7 +66,7 @@ async function makeProvider(name) {
const made = await request(h.base, 'POST', '/api/v1/providers', {
tok: service,
body: {
-- key: name, kind: offer.kind, base_url: `${upstream.url}/v1`, auth_mode: 'none',
++ key: name, status: 'active', kind: offer.kind, base_url: `${upstream.url}/v1`, auth_mode: 'none',
capabilities: anthropic ? ['chat', 'messages', 'tools', 'json'] : ['chat', 'complete', 'tools', 'json'],
default_model: offer.model,
provider_access: 'OWN_DEPLOYMENT', metadata: { data_policy: offer.data_policy },
ddiff --git a/test/exchange-read.test.js b/test/exchange-read.test.js
iindex 6ada329..5344be8 100644
---- a/test/exchange-read.test.js
++++ b/test/exchange-read.test.js
@@@ -56,7 +56,7 @@ t.test('catalog groups the same model across providers, with prices, speed, upti
await h.registry.upsertModel({ provider_key: 'deepseek', model_key: 'deepseek-chat', type: 'chat', context_window: 131072,
cost: { in_per_mtok: 0.28, out_per_mtok: 0.42, cached_per_mtok: 0.028 } });
await h.registry.upsertProvider({
-- key: 'deepinfra', display_name: 'DeepInfra', kind: 'openai', auth_mode: 'bearer', secret_ref: 'env:DEEPINFRA_KEY',
++ key: 'deepinfra', status: 'active', display_name: 'DeepInfra', kind: 'openai', auth_mode: 'bearer', secret_ref: 'env:DEEPINFRA_KEY',
base_url: 'https://api.deepinfra.test', capabilities: ['chat'], provider_access: 'AUTHORIZED_WHOLESALE',
});
await h.registry.upsertModel({ provider_key: 'deepinfra', model_key: 'deepseek-chat', type: 'chat', context_window: 131072,
ddiff --git a/test/prefer-route.test.js b/test/prefer-route.test.js
iindex 90ef867..72be8c8 100644
---- a/test/prefer-route.test.js
++++ b/test/prefer-route.test.js
@@@ -28,7 +28,7 @@ const credentialFetch = async (url, init = {}) => {
};
async function provider(body) {
-- const r = await request(h.base, 'POST', '/api/v1/providers', { tok: service, body: { kind: 'http', auth_mode: 'none', timeout_ms: 3000, ...body } });
++ const r = await request(h.base, 'POST', '/api/v1/providers', { tok: service, body: { status: 'active', kind: 'http', auth_mode: 'none', timeout_ms: 3000, ...body } });
assert.strictEqual(r.status, 201, r.text);
}
async function model(providerKey, modelKey, body) {
ddiff --git a/test/price-book.test.js b/test/price-book.test.js
iindex d005341..76d97c7 100644
---- a/test/price-book.test.js
++++ b/test/price-book.test.js
@@@ -158,7 +158,7 @@ t.test('the owner approves a waiting change and the price the router charges fol
t.test('the public catalog says when the price was checked', async () => {
// The catalog lists an offer only when the provider's key is there, as production has it.
await h.registry.upsertProvider({
-- key: 'deepinfra', display_name: 'DeepInfra', kind: 'openai', auth_mode: 'bearer', secret_ref: 'env:DEEPINFRA_KEY',
++ key: 'deepinfra', status: 'active', display_name: 'DeepInfra', kind: 'openai', auth_mode: 'bearer', secret_ref: 'env:DEEPINFRA_KEY',
base_url: 'https://api.deepinfra.test', capabilities: ['chat'], provider_access: 'AUTHORIZED_WHOLESALE',
});
const book = createPriceBook({ db: h.db, registry: h.registry, config: { priceBook: { enabled: true }, nodeEnv: 'test' }, clock: CLOCK, log: silent, transport: recorded });