From df86624cc3c8eeb92bda99ab6060ea068210c4f8 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Fri, 9 Oct 2026 17:57:24 +0530 Subject: [PATCH 01/14] feat(providers): add Azure Foundry and Bedrock catalog entries --- .../settings/provider-display-copy.ts | 10 +++++++ packages/core/src/provider-registry.ts | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index 766cffa4f4a..07d6ac0b412 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -259,6 +259,16 @@ export const PROVIDER_DISPLAY_COPY = { 'zh-TW': { name: 'Cloudflare Workers AI', description: 'Cloudflare 託管模型,使用帳號層級連線', badge: 'API' }, en: { name: 'Cloudflare Workers AI', description: 'Cloudflare-hosted models over the account-scoped API.', badge: 'API' }, }, + 'azure-foundry': { + 'zh-CN': { name: 'Azure AI Foundry', description: '使用 Foundry 终结点和 API 密钥连接已部署的 OpenAI 兼容模型。', badge: 'API' }, + 'zh-TW': { name: 'Azure AI Foundry', description: '使用 Foundry 端點與 API 金鑰連線已部署的 OpenAI 相容模型。', badge: 'API' }, + en: { name: 'Azure AI Foundry', description: 'Connect a deployed OpenAI-compatible model with its Foundry endpoint and API key.', badge: 'API' }, + }, + 'amazon-bedrock': { + 'zh-CN': { name: 'Amazon Bedrock', description: '通过 Bedrock 的 OpenAI 兼容 API、区域终结点和 Bedrock API 密钥连接模型。', badge: 'API' }, + 'zh-TW': { name: 'Amazon Bedrock', description: '透過 Bedrock 的 OpenAI 相容 API、區域端點與 Bedrock API 金鑰連線模型。', badge: 'API' }, + en: { name: 'Amazon Bedrock', description: 'Connect through Bedrock’s OpenAI-compatible API, regional endpoint, and Bedrock API key.', badge: 'API' }, + }, huggingface: { 'zh-CN': { name: 'Hugging Face', description: 'Inference Providers 路由,聚合多家托管模型', badge: '路由' }, 'zh-TW': { name: 'Hugging Face', description: 'Inference Providers 路由,聚合多家託管模型', badge: '路由' }, diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index 41441941ac4..a252127aa55 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -1525,6 +1525,34 @@ const providerRegistry = { signupUrl: 'https://dash.cloudflare.com/profile/api-tokens', catalogOrder: 33, }, + 'azure-foundry': { + label: 'Azure AI Foundry', + baseUrl: '', + authKind: 'api_key', + fallbackModels: [], + status: 'ready', + runtimeAdapter: { kind: 'openai-compatible', requireBaseUrl: true }, + modelDiscovery: { kind: 'protocol' }, + category: 'overseas', + catalogGroup: 'api', + signupUrl: 'https://ai.azure.com/', + catalogOrder: 42, + }, + 'amazon-bedrock': { + label: 'Amazon Bedrock', + baseUrl: '', + authKind: 'api_key', + fallbackModels: [], + status: 'ready', + runtimeAdapter: { kind: 'openai-compatible', requireBaseUrl: true }, + // Bedrock's OpenAI-compatible endpoint and model-list availability vary + // by endpoint family and region. A custom endpoint remains user supplied. + modelDiscovery: { kind: 'protocol' }, + category: 'overseas', + catalogGroup: 'api', + signupUrl: 'https://console.aws.amazon.com/bedrock/', + catalogOrder: 43, + }, 'ollama-cloud': { label: ollamaCloud.name, baseUrl: ollamaCloud.api, From 75c212a0ea3cdad14b1e3e3033c3a4afcd174f13 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Fri, 9 Oct 2026 23:47:35 +0530 Subject: [PATCH 02/14] fix(providers): mark scoped endpoints as custom --- .../src/__tests__/provider-catalog-contract.test.ts | 10 ++++++++++ packages/core/src/provider-registry.ts | 12 ++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/core/src/__tests__/provider-catalog-contract.test.ts b/packages/core/src/__tests__/provider-catalog-contract.test.ts index bce88beea88..5f48bd89f5d 100644 --- a/packages/core/src/__tests__/provider-catalog-contract.test.ts +++ b/packages/core/src/__tests__/provider-catalog-contract.test.ts @@ -66,6 +66,16 @@ describe('Moonshot provider regions', () => { }); describe('provider catalog contract — structural invariants over CATALOG_PROVIDER_TYPES', () => { + it('marks tenant- or region-scoped API providers as user-endpoint connections', () => { + for (const type of ['azure-foundry', 'amazon-bedrock'] as const) { + const definition = PROVIDER_REGISTRY[type]; + assert.equal(definition.authKind, 'api_key'); + assert.equal(definition.baseUrl, ''); + assert.equal(definition.runtimeAdapter.requireBaseUrl, true); + assert.equal(definition.category, 'custom'); + } + }); + it('exposes an endpoint source that passes the production baseUrl gate', () => { for (const type of CATALOG_PROVIDER_TYPES) { const def = PROVIDER_REGISTRY[type]; diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index a252127aa55..a72b14f7e97 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -739,7 +739,8 @@ const providerRegistry = { status: 'ready', runtimeAdapter: { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, modelDiscovery: { kind: 'protocol' }, - category: 'overseas', + // Foundry endpoints are resource-specific and must be supplied by the user. + category: 'custom', catalogGroup: 'api', signupUrl: 'https://console.anthropic.com/settings/keys', catalogOrder: 9, @@ -776,7 +777,8 @@ const providerRegistry = { status: 'ready', runtimeAdapter: { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, modelDiscovery: { kind: 'protocol' }, - category: 'overseas', + // Bedrock endpoint selection is region/account-specific and user supplied. + category: 'custom', catalogGroup: 'plans', signupUrl: 'https://platform.minimax.io/subscribe/coding-plan', catalogOrder: 2, @@ -1533,7 +1535,8 @@ const providerRegistry = { status: 'ready', runtimeAdapter: { kind: 'openai-compatible', requireBaseUrl: true }, modelDiscovery: { kind: 'protocol' }, - category: 'overseas', + // Foundry endpoints are resource-specific and must be supplied by the user. + category: 'custom', catalogGroup: 'api', signupUrl: 'https://ai.azure.com/', catalogOrder: 42, @@ -1548,7 +1551,8 @@ const providerRegistry = { // Bedrock's OpenAI-compatible endpoint and model-list availability vary // by endpoint family and region. A custom endpoint remains user supplied. modelDiscovery: { kind: 'protocol' }, - category: 'overseas', + // Bedrock endpoint selection is region/account-specific and user supplied. + category: 'custom', catalogGroup: 'api', signupUrl: 'https://console.aws.amazon.com/bedrock/', catalogOrder: 43, From f230577477327dfbde3d06270541242854192d9c Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Fri, 9 Oct 2026 23:48:18 +0530 Subject: [PATCH 03/14] fix(providers): preserve unrelated provider categories --- packages/core/src/provider-registry.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index a72b14f7e97..37868b626a9 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -739,8 +739,7 @@ const providerRegistry = { status: 'ready', runtimeAdapter: { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, modelDiscovery: { kind: 'protocol' }, - // Foundry endpoints are resource-specific and must be supplied by the user. - category: 'custom', + category: 'overseas', catalogGroup: 'api', signupUrl: 'https://console.anthropic.com/settings/keys', catalogOrder: 9, @@ -777,8 +776,7 @@ const providerRegistry = { status: 'ready', runtimeAdapter: { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, modelDiscovery: { kind: 'protocol' }, - // Bedrock endpoint selection is region/account-specific and user supplied. - category: 'custom', + category: 'overseas', catalogGroup: 'plans', signupUrl: 'https://platform.minimax.io/subscribe/coding-plan', catalogOrder: 2, From 02f255be5cd22c5d87af3af8337a0e86f6dcc8c5 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sat, 10 Oct 2026 00:06:11 +0530 Subject: [PATCH 04/14] test(providers): narrow endpoint adapter assertion --- .../core/src/__tests__/provider-catalog-contract.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/__tests__/provider-catalog-contract.test.ts b/packages/core/src/__tests__/provider-catalog-contract.test.ts index 5f48bd89f5d..315c777839b 100644 --- a/packages/core/src/__tests__/provider-catalog-contract.test.ts +++ b/packages/core/src/__tests__/provider-catalog-contract.test.ts @@ -71,7 +71,10 @@ describe('provider catalog contract — structural invariants over CATALOG_PROVI const definition = PROVIDER_REGISTRY[type]; assert.equal(definition.authKind, 'api_key'); assert.equal(definition.baseUrl, ''); - assert.equal(definition.runtimeAdapter.requireBaseUrl, true); + assert.ok( + definition.runtimeAdapter.kind === 'openai-compatible' && + definition.runtimeAdapter.requireBaseUrl, + ); assert.equal(definition.category, 'custom'); } }); From 579da6ba076ecdb9b211d19f1366adf7efb620fb Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sat, 10 Oct 2026 11:27:49 +0530 Subject: [PATCH 05/14] feat(providers): add protocol support for cloud endpoints --- .../provider-endpoint-presentation.test.ts | 5 ++++ .../provider-endpoint-field.tsx | 28 ++++++++++++++++-- .../renderer/settings/provider-add-form.tsx | 6 ++-- .../settings/provider-display-copy.ts | 4 +-- .../provider-catalog-contract.test.ts | 23 ++++++++++++++- .../__tests__/runtime-policy-codec.test.ts | 29 +++++++++++++++++-- packages/core/src/provider-registry.ts | 18 +++++++++++- .../connection-catalog-codec.ts | 21 +++++++++++--- 8 files changed, 118 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/main/__tests__/provider-endpoint-presentation.test.ts b/apps/desktop/src/main/__tests__/provider-endpoint-presentation.test.ts index 741c872a95a..28b74b0219a 100644 --- a/apps/desktop/src/main/__tests__/provider-endpoint-presentation.test.ts +++ b/apps/desktop/src/main/__tests__/provider-endpoint-presentation.test.ts @@ -215,6 +215,10 @@ test('draft request previews follow the relay protocol, custom prefixes and endp 'https://relay.example/proxy/responses'); assert.equal(providerRequestUrlPreview('custom', 'https://relay.example/', 'openai-responses'), 'https://relay.example/responses'); + assert.equal(providerRequestUrlPreview('azure-foundry', 'https://tenant.example/openai/v1'), + 'https://tenant.example/openai/v1/chat/completions'); + assert.equal(providerRequestUrlPreview('amazon-bedrock-api-key', 'https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1', 'openai-responses'), + 'https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1/responses'); }); test('switching a custom connection protocol replaces the full OpenAI endpoint', () => { @@ -238,5 +242,6 @@ test('empty, incomplete, unsaveable and unsupported protocol drafts have no requ assert.equal(providerRequestUrlPreview('custom', draft), null, draft); } assert.equal(providerRequestUrlPreview('openai', 'https://relay.example/v1'), null); + assert.equal(providerRequestUrlPreview('amazon-bedrock-api-key', 'https://bedrock-runtime.us-east-1.amazonaws.com/anthropic', 'anthropic-messages'), null); assert.equal(providerRequestUrlPreview('custom', 'https://relay.example/v1', 'anthropic-messages'), null); }); diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx b/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx index b470a62004d..5de2b2eeff2 100644 --- a/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx +++ b/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx @@ -31,16 +31,35 @@ export function ProviderEndpointField(props: { apiProtocol?: ModelApiProtocol; children(description: string | undefined): ReactNode; }) { - const copy = getProviderSettingsCopy(useUiLocale()).shared; + const locale = useUiLocale(); + const copy = getProviderSettingsCopy(locale).shared; const url = providerRequestUrlPreview(props.providerType, props.baseUrl, props.apiProtocol); - if (props.providerType !== 'custom') return props.children(undefined); + const supportsPreview = props.providerType === 'custom' || props.providerType === 'azure-foundry' || + props.providerType === 'amazon-bedrock-api-key'; + if (!supportsPreview) return props.children(undefined); const description = url ? `${copy.requestUrlLabel} ${url}` : undefined; + const endpointHelp = props.providerType === 'azure-foundry' + ? locale === 'en' + ? 'Use the deployed model endpoint ending in /openai/v1. Enter the deployment name as the model ID; the models list may only contain base models.' + : locale === 'zh-CN' + ? '使用以 /openai/v1 结尾的已部署模型终结点。模型 ID 请填写部署名称;模型列表可能只包含基础模型。' + : '請使用以 /openai/v1 結尾的已部署模型端點。模型 ID 請填寫部署名稱;模型清單可能只包含基礎模型。' + : props.providerType === 'amazon-bedrock-api-key' + ? (props.apiProtocol === 'anthropic-messages' + ? locale === 'en' + ? 'For Anthropic Messages, use https://bedrock-runtime.{region}.amazonaws.com/anthropic. Enter the Bedrock model ID manually if it is not listed.' + : 'Anthropic Messages 請使用 https://bedrock-runtime.{region}.amazonaws.com/anthropic。若未列出,請手動輸入 Bedrock 模型 ID。' + : locale === 'en' + ? 'For Chat Completions or Responses, use https://bedrock-runtime.{region}.amazonaws.com/openai/v1. Enter the Bedrock model ID manually if it is not listed.' + : 'Chat Completions 或 Responses 請使用 https://bedrock-runtime.{region}.amazonaws.com/openai/v1。若未列出,請手動輸入 Bedrock 模型 ID。') + : undefined; // Astryx's description is above the input (and hidden with its label). // This computed output belongs below it; pass it through aria-description // on the control as well, without duplicating the field's visible label. return (
{props.children(description)} + {endpointHelp &&

{endpointHelp}

} {description && }
); @@ -52,7 +71,10 @@ export function providerRequestUrlPreview( draftBaseUrl: string, apiProtocol: ModelApiProtocol = 'openai-chat', ): string | null { - if (providerType !== 'custom' || apiProtocol === 'anthropic-messages') { + if ( + !['custom', 'azure-foundry', 'amazon-bedrock-api-key'].includes(providerType) || + apiProtocol === 'anthropic-messages' + ) { return null; } // A draft must be a complete, saveable HTTP(S) address. Do not substitute diff --git a/apps/desktop/src/renderer/settings/provider-add-form.tsx b/apps/desktop/src/renderer/settings/provider-add-form.tsx index 80aafac7224..0df0b5e091e 100644 --- a/apps/desktop/src/renderer/settings/provider-add-form.tsx +++ b/apps/desktop/src/renderer/settings/provider-add-form.tsx @@ -132,6 +132,8 @@ export function AddProviderForm(props: { }>({ baseUrl: defaults.baseUrl, defaultApiProtocol: 'openai-chat' }); const { baseUrl, defaultApiProtocol } = endpoint; const isCustom = props.providerType === 'custom'; + const canChooseApiProtocol = isCustom || props.providerType === 'azure-foundry' || + props.providerType === 'amazon-bedrock-api-key'; const [cloudflareAccountId, setCloudflareAccountId] = useState(''); const [apiKey, setApiKey] = useState(''); const [defaultModel, setDefaultModel] = useState(recommendedDefaultModel); @@ -383,7 +385,7 @@ export function AddProviderForm(props: { name: name || display.name, providerType: props.providerType, baseUrl: resolvedBaseUrl, - ...(isCustom ? { defaultApiProtocol } : {}), + ...(canChooseApiProtocol ? { defaultApiProtocol } : {}), defaultModel: createdDefaultModel, ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), ...(Object.keys(normalizedRequestHeaders).length > 0 @@ -782,7 +784,7 @@ export function AddProviderForm(props: { )} )} - {isCustom && ( + {canChooseApiProtocol && ( { describe('provider catalog contract — structural invariants over CATALOG_PROVIDER_TYPES', () => { it('marks tenant- or region-scoped API providers as user-endpoint connections', () => { - for (const type of ['azure-foundry', 'amazon-bedrock'] as const) { + for (const type of ['azure-foundry', 'amazon-bedrock-api-key'] as const) { const definition = PROVIDER_REGISTRY[type]; assert.equal(definition.authKind, 'api_key'); assert.equal(definition.baseUrl, ''); @@ -76,6 +76,27 @@ describe('provider catalog contract — structural invariants over CATALOG_PROVI definition.runtimeAdapter.requireBaseUrl, ); assert.equal(definition.category, 'custom'); + assert.deepEqual(Object.keys(definition.protocolAdapters ?? {}).sort(), [ + 'anthropic-messages', + 'openai-responses', + ]); + } + }); + + it('declares a working runtime adapter for every selectable API protocol', () => { + for (const type of ['azure-foundry', 'amazon-bedrock-api-key'] as const) { + const definition = PROVIDER_REGISTRY[type]; + assert.equal(definition.runtimeAdapter.kind, 'openai-compatible'); + assert.deepEqual(definition.protocolAdapters?.['openai-responses'], { + kind: 'openai', + apiProtocol: 'openai-responses', + responses: { adapter: 'openai', reasoningReplay: 'encrypted-content' }, + }); + assert.deepEqual(definition.protocolAdapters?.['anthropic-messages'], { + kind: 'anthropic', + auth: 'api-key', + normalizeBaseUrl: true, + }); } }); diff --git a/packages/core/src/__tests__/runtime-policy-codec.test.ts b/packages/core/src/__tests__/runtime-policy-codec.test.ts index 521cae3f618..1b9d1b09e0a 100644 --- a/packages/core/src/__tests__/runtime-policy-codec.test.ts +++ b/packages/core/src/__tests__/runtime-policy-codec.test.ts @@ -439,7 +439,8 @@ test('relay model profiles round-trip canonical entries and drafts, strictly', ( null, ); - // A custom connection needs a default wire; no other provider may carry one. + // Custom endpoints and the new protocol-flexible catalog entries can store + // a chosen default wire; other providers still cannot claim unsupported wires. const custom = { slug: 'relay', name: 'Relay', @@ -454,8 +455,8 @@ test('relay model profiles round-trip canonical entries and drafts, strictly', ( [withoutProtocol, /default API protocol is invalid/], [{ ...custom, defaultApiProtocol: 'google-generate' }, /default API protocol is invalid/], [ - { ...custom, providerType: 'openai', baseUrl: undefined }, - /only a custom connection has a default API protocol/, + { ...custom, providerType: 'anthropic', baseUrl: undefined }, + /provider does not support this default API protocol/, ], ] as const) { assert.throws( @@ -463,6 +464,28 @@ test('relay model profiles round-trip canonical entries and drafts, strictly', ( message, ); } + for (const providerType of ['azure-foundry', 'amazon-bedrock-api-key'] as const) { + for (const defaultApiProtocol of [ + 'openai-chat', + 'openai-responses', + 'anthropic-messages', + ] as const) { + assert.doesNotThrow(() => + normalizeCreateCatalogConnectionInput({ + expectedCatalogRevision: 0, + connection: { + slug: providerType, + name: providerType, + providerType, + baseUrl: 'https://provider.example/openai/v1', + defaultApiProtocol, + enabled: true, + enabledModelIds: [], + }, + }), + ); + } + } assert.deepEqual( normalizeConnectionCatalogEntryUpdate({ name: 'Relay', diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index 37868b626a9..3090589a790 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -1532,6 +1532,14 @@ const providerRegistry = { fallbackModels: [], status: 'ready', runtimeAdapter: { kind: 'openai-compatible', requireBaseUrl: true }, + protocolAdapters: { + 'openai-responses': { + kind: 'openai', + apiProtocol: 'openai-responses', + responses: { adapter: 'openai', reasoningReplay: 'encrypted-content' }, + }, + 'anthropic-messages': { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, + }, modelDiscovery: { kind: 'protocol' }, // Foundry endpoints are resource-specific and must be supplied by the user. category: 'custom', @@ -1539,13 +1547,21 @@ const providerRegistry = { signupUrl: 'https://ai.azure.com/', catalogOrder: 42, }, - 'amazon-bedrock': { + 'amazon-bedrock-api-key': { label: 'Amazon Bedrock', baseUrl: '', authKind: 'api_key', fallbackModels: [], status: 'ready', runtimeAdapter: { kind: 'openai-compatible', requireBaseUrl: true }, + protocolAdapters: { + 'openai-responses': { + kind: 'openai', + apiProtocol: 'openai-responses', + responses: { adapter: 'openai', reasoningReplay: 'encrypted-content' }, + }, + 'anthropic-messages': { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, + }, // Bedrock's OpenAI-compatible endpoint and model-list availability vary // by endpoint family and region. A custom endpoint remains user supplied. modelDiscovery: { kind: 'protocol' }, diff --git a/packages/core/src/runtime-policy/connection-catalog-codec.ts b/packages/core/src/runtime-policy/connection-catalog-codec.ts index 2177e7f55ba..54fc00580e3 100644 --- a/packages/core/src/runtime-policy/connection-catalog-codec.ts +++ b/packages/core/src/runtime-policy/connection-catalog-codec.ts @@ -400,14 +400,27 @@ export function decodeDefaultApiProtocol( value: unknown, providerType: ProviderType, ): ModelApiProtocol | undefined { - if (providerType !== 'custom') { - if (value !== undefined) { - throw domainError('only a custom connection has a default API protocol'); + const supportsProtocol = (protocol: ModelApiProtocol): boolean => + protocol === 'openai-chat' + ? providerType === 'custom' || + PROVIDER_REGISTRY[providerType].runtimeAdapter.kind === 'openai-compatible' || + PROVIDER_REGISTRY[providerType].protocolAdapters?.[protocol] !== undefined + : PROVIDER_REGISTRY[providerType].protocolAdapters?.[protocol] !== undefined; + if (value === undefined) { + if (providerType === 'custom') { + throw domainError('custom connection default API protocol is invalid'); } return undefined; } if (!isModelApiProtocol(value)) { - throw domainError('custom connection default API protocol is invalid'); + throw domainError('connection default API protocol is invalid'); + } + if (!supportsProtocol(value)) { + throw domainError( + providerType === 'custom' + ? 'custom connection default API protocol is invalid' + : 'provider does not support this default API protocol', + ); } return value; } From b1b9121c0dcfbbe6ed9b785893f44a3223bd50ce Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sat, 10 Oct 2026 11:35:14 +0530 Subject: [PATCH 06/14] fix(providers): localize cloud endpoint guidance --- .../provider-endpoint-field.tsx | 19 +++++-------------- .../settings-provider-copy.ts | 9 +++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx b/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx index 5de2b2eeff2..1bd3aca112f 100644 --- a/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx +++ b/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx @@ -31,27 +31,18 @@ export function ProviderEndpointField(props: { apiProtocol?: ModelApiProtocol; children(description: string | undefined): ReactNode; }) { - const locale = useUiLocale(); - const copy = getProviderSettingsCopy(locale).shared; + const copy = getProviderSettingsCopy(useUiLocale()).shared; const url = providerRequestUrlPreview(props.providerType, props.baseUrl, props.apiProtocol); const supportsPreview = props.providerType === 'custom' || props.providerType === 'azure-foundry' || props.providerType === 'amazon-bedrock-api-key'; if (!supportsPreview) return props.children(undefined); const description = url ? `${copy.requestUrlLabel} ${url}` : undefined; const endpointHelp = props.providerType === 'azure-foundry' - ? locale === 'en' - ? 'Use the deployed model endpoint ending in /openai/v1. Enter the deployment name as the model ID; the models list may only contain base models.' - : locale === 'zh-CN' - ? '使用以 /openai/v1 结尾的已部署模型终结点。模型 ID 请填写部署名称;模型列表可能只包含基础模型。' - : '請使用以 /openai/v1 結尾的已部署模型端點。模型 ID 請填寫部署名稱;模型清單可能只包含基礎模型。' + ? copy.azureFoundryEndpointHelp : props.providerType === 'amazon-bedrock-api-key' - ? (props.apiProtocol === 'anthropic-messages' - ? locale === 'en' - ? 'For Anthropic Messages, use https://bedrock-runtime.{region}.amazonaws.com/anthropic. Enter the Bedrock model ID manually if it is not listed.' - : 'Anthropic Messages 請使用 https://bedrock-runtime.{region}.amazonaws.com/anthropic。若未列出,請手動輸入 Bedrock 模型 ID。' - : locale === 'en' - ? 'For Chat Completions or Responses, use https://bedrock-runtime.{region}.amazonaws.com/openai/v1. Enter the Bedrock model ID manually if it is not listed.' - : 'Chat Completions 或 Responses 請使用 https://bedrock-runtime.{region}.amazonaws.com/openai/v1。若未列出,請手動輸入 Bedrock 模型 ID。') + ? props.apiProtocol === 'anthropic-messages' + ? copy.bedrockAnthropicEndpointHelp + : copy.bedrockOpenAiEndpointHelp : undefined; // Astryx's description is above the input (and hidden with its label). // This computed output belongs below it; pass it through aria-description diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index 19df1d16644..321338e340b 100644 --- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts @@ -273,6 +273,9 @@ const zhCopy = { }, shared: { requestUrlLabel: '请求地址:', + azureFoundryEndpointHelp: '使用以 /openai/v1 结尾的已部署模型终结点。模型 ID 请填写部署名称;模型列表可能只包含基础模型。', + bedrockOpenAiEndpointHelp: 'Chat Completions 或 Responses 请使用 https://bedrock-runtime.{region}.amazonaws.com/openai/v1。若未列出,请手动输入 Bedrock 模型 ID。', + bedrockAnthropicEndpointHelp: 'Anthropic Messages 请使用 https://bedrock-runtime.{region}.amazonaws.com/anthropic。若未列出,请手动输入 Bedrock 模型 ID。', connectionStale: '连接状态已更新,请刷新列表后再删除。', actionFallback: '模型连接服务暂时不可用,请稍后重试。', rateLimit: '当前账号或模型服务触发速率限制,请稍后重试。', timeout: '请求超时,请检查网络或代理后重试。', unavailable: '模型服务暂时不可用,请稍后重试。', @@ -486,6 +489,9 @@ const zhTwCopy = { }, shared: { requestUrlLabel: '請求地址:', + azureFoundryEndpointHelp: '請使用以 /openai/v1 結尾的已部署模型端點。模型 ID 請填寫部署名稱;模型清單可能只包含基礎模型。', + bedrockOpenAiEndpointHelp: 'Chat Completions 或 Responses 請使用 https://bedrock-runtime.{region}.amazonaws.com/openai/v1。若未列出,請手動輸入 Bedrock 模型 ID。', + bedrockAnthropicEndpointHelp: 'Anthropic Messages 請使用 https://bedrock-runtime.{region}.amazonaws.com/anthropic。若未列出,請手動輸入 Bedrock 模型 ID。', connectionStale: '連線狀態已更新,請重新整理清單後再刪除。', actionFallback: '模型連線服務暫時不可用,請稍後重試。', rateLimit: '目前帳號或模型服務觸發速率限制,請稍後重試。', timeout: '請求超時,請檢查網路或代理後重試。', unavailable: '模型服務暫時不可用,請稍後重試。', @@ -698,6 +704,9 @@ const enCopy: ProviderSettingsCopy = { }, shared: { requestUrlLabel: 'Request URL:', + azureFoundryEndpointHelp: 'Use the deployed model endpoint ending in /openai/v1. Enter the deployment name as the model ID; the models list may only contain base models.', + bedrockOpenAiEndpointHelp: 'For Chat Completions or Responses, use https://bedrock-runtime.{region}.amazonaws.com/openai/v1. Enter the Bedrock model ID manually if it is not listed.', + bedrockAnthropicEndpointHelp: 'For Anthropic Messages, use https://bedrock-runtime.{region}.amazonaws.com/anthropic. Enter the Bedrock model ID manually if it is not listed.', connectionStale: 'The connection changed while deleting. Refresh the list and try again.', actionFallback: 'The model connection service is temporarily unavailable. Try again later.', rateLimit: 'This account or model service is rate-limited. Try again later.', timeout: 'The request timed out. Check the network or proxy and try again.', unavailable: 'The model service is temporarily unavailable. Try again later.', From 3d7ca4bdc961860bc9d665482f5ac572a3a9ae58 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sat, 10 Oct 2026 11:43:42 +0530 Subject: [PATCH 07/14] test(providers): pin Responses adapter contracts --- .../src/__tests__/provider-catalog-contract.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/core/src/__tests__/provider-catalog-contract.test.ts b/packages/core/src/__tests__/provider-catalog-contract.test.ts index be1deff66d0..544cabf8731 100644 --- a/packages/core/src/__tests__/provider-catalog-contract.test.ts +++ b/packages/core/src/__tests__/provider-catalog-contract.test.ts @@ -208,6 +208,16 @@ describe('provider catalog contract — structural invariants over CATALOG_PROVI compatibility: 'alibaba-token-plan', }, }, + { + providerType: 'azure-foundry', + via: 'protocolAdapters.openai-responses', + contract: { adapter: 'openai', reasoningReplay: 'encrypted-content' }, + }, + { + providerType: 'amazon-bedrock-api-key', + via: 'protocolAdapters.openai-responses', + contract: { adapter: 'openai', reasoningReplay: 'encrypted-content' }, + }, { providerType: 'custom', via: 'protocolAdapters.openai-responses', From bac2f468900740ea80e0ce269f321fd75803d021 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sat, 10 Oct 2026 11:59:53 +0530 Subject: [PATCH 08/14] fix(providers): scope protocol selection to new endpoints --- .../runtime-policy/connection-catalog-codec.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/core/src/runtime-policy/connection-catalog-codec.ts b/packages/core/src/runtime-policy/connection-catalog-codec.ts index 54fc00580e3..f5fd3b33b09 100644 --- a/packages/core/src/runtime-policy/connection-catalog-codec.ts +++ b/packages/core/src/runtime-policy/connection-catalog-codec.ts @@ -400,12 +400,17 @@ export function decodeDefaultApiProtocol( value: unknown, providerType: ProviderType, ): ModelApiProtocol | undefined { + const canChooseProtocol = + providerType === 'custom' || + providerType === 'azure-foundry' || + providerType === 'amazon-bedrock-api-key'; const supportsProtocol = (protocol: ModelApiProtocol): boolean => - protocol === 'openai-chat' - ? providerType === 'custom' || - PROVIDER_REGISTRY[providerType].runtimeAdapter.kind === 'openai-compatible' || - PROVIDER_REGISTRY[providerType].protocolAdapters?.[protocol] !== undefined - : PROVIDER_REGISTRY[providerType].protocolAdapters?.[protocol] !== undefined; + providerType === 'custom' + ? isModelApiProtocol(protocol) + : canChooseProtocol && + (protocol === 'openai-chat' + ? PROVIDER_REGISTRY[providerType].runtimeAdapter.kind === 'openai-compatible' + : PROVIDER_REGISTRY[providerType].protocolAdapters?.[protocol] !== undefined); if (value === undefined) { if (providerType === 'custom') { throw domainError('custom connection default API protocol is invalid'); From 0dc2f71c69cc6f4b95e3f3a677ce75a1006caa98 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sat, 10 Oct 2026 13:16:39 +0530 Subject: [PATCH 09/14] fix(providers): align protocol help and registry gates --- .../provider-endpoint-presentation.test.ts | 21 ++++++++++- .../provider-endpoint-field.tsx | 37 ++++++++++++++----- .../settings-provider-copy.ts | 3 ++ .../features/connection-settings/testing.ts | 2 +- .../renderer/settings/provider-add-form.tsx | 4 +- .../settings/provider-display-copy.ts | 4 +- .../provider-catalog-contract.test.ts | 19 +++++++++- packages/core/src/llm-connections.ts | 4 ++ packages/core/src/provider-registry.ts | 27 +++++++++++++- .../connection-catalog-codec.ts | 12 +----- 10 files changed, 105 insertions(+), 28 deletions(-) diff --git a/apps/desktop/src/main/__tests__/provider-endpoint-presentation.test.ts b/apps/desktop/src/main/__tests__/provider-endpoint-presentation.test.ts index 28b74b0219a..2b6eb9f464f 100644 --- a/apps/desktop/src/main/__tests__/provider-endpoint-presentation.test.ts +++ b/apps/desktop/src/main/__tests__/provider-endpoint-presentation.test.ts @@ -25,7 +25,10 @@ import { providerEndpointPresentation, } from '../../renderer/settings/provider-endpoint-presentation.js'; -import { providerRequestUrlPreview } from '../../renderer/features/connection-settings/testing.js'; +import { + providerEndpointHelpKey, + providerRequestUrlPreview, +} from '../../renderer/features/connection-settings/testing.js'; // A 40-char hex-shaped run, built rather than written: long enough to trip // the display redactor's long-opaque-token rule wherever it is left alone. @@ -221,6 +224,22 @@ test('draft request previews follow the relay protocol, custom prefixes and endp 'https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1/responses'); }); +test('provider endpoint help follows the selected Azure and Bedrock API protocol', () => { + assert.equal(providerEndpointHelpKey('azure-foundry', 'openai-chat'), 'azureFoundryEndpointHelp'); + assert.equal( + providerEndpointHelpKey('azure-foundry', 'anthropic-messages'), + 'azureFoundryAnthropicEndpointHelp', + ); + assert.equal( + providerEndpointHelpKey('amazon-bedrock-api-key', 'anthropic-messages'), + 'bedrockAnthropicEndpointHelp', + ); + assert.equal( + providerEndpointHelpKey('amazon-bedrock-api-key', 'openai-responses'), + 'bedrockOpenAiEndpointHelp', + ); +}); + test('switching a custom connection protocol replaces the full OpenAI endpoint', () => { assert.equal(providerRequestUrlPreview('custom', 'https://relay.example/proxy/chat/completions', 'openai-responses'), 'https://relay.example/proxy/responses'); diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx b/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx index 1bd3aca112f..15d0a90c3d7 100644 --- a/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx +++ b/apps/desktop/src/renderer/features/connection-settings/provider-endpoint-field.tsx @@ -24,6 +24,7 @@ import { getProviderSettingsCopy } from './settings-provider-copy.js'; import { openAiChatUrl, openResponsesUrl } from '@maka/core/openai-urls'; import { normalizeCatalogConnectionBaseUrl } from '@maka/core/runtime-policy'; import { redactSecrets } from '@maka/core/display-redaction'; +import { providerSupportsApiProtocolSelection } from '@maka/core/llm-connections'; export function ProviderEndpointField(props: { providerType: ProviderType; @@ -33,17 +34,11 @@ export function ProviderEndpointField(props: { }) { const copy = getProviderSettingsCopy(useUiLocale()).shared; const url = providerRequestUrlPreview(props.providerType, props.baseUrl, props.apiProtocol); - const supportsPreview = props.providerType === 'custom' || props.providerType === 'azure-foundry' || - props.providerType === 'amazon-bedrock-api-key'; + const supportsPreview = providerSupportsApiProtocolSelection(props.providerType); if (!supportsPreview) return props.children(undefined); const description = url ? `${copy.requestUrlLabel} ${url}` : undefined; - const endpointHelp = props.providerType === 'azure-foundry' - ? copy.azureFoundryEndpointHelp - : props.providerType === 'amazon-bedrock-api-key' - ? props.apiProtocol === 'anthropic-messages' - ? copy.bedrockAnthropicEndpointHelp - : copy.bedrockOpenAiEndpointHelp - : undefined; + const endpointHelpKey = providerEndpointHelpKey(props.providerType, props.apiProtocol); + const endpointHelp = endpointHelpKey ? copy[endpointHelpKey] : undefined; // Astryx's description is above the input (and hidden with its label). // This computed output belongs below it; pass it through aria-description // on the control as well, without duplicating the field's visible label. @@ -56,6 +51,28 @@ export function ProviderEndpointField(props: { ); } +export function providerEndpointHelpKey( + providerType: ProviderType, + apiProtocol?: ModelApiProtocol, +): + | 'azureFoundryEndpointHelp' + | 'azureFoundryAnthropicEndpointHelp' + | 'bedrockOpenAiEndpointHelp' + | 'bedrockAnthropicEndpointHelp' + | undefined { + if (providerType === 'azure-foundry') { + return apiProtocol === 'anthropic-messages' + ? 'azureFoundryAnthropicEndpointHelp' + : 'azureFoundryEndpointHelp'; + } + if (providerType === 'amazon-bedrock-api-key') { + return apiProtocol === 'anthropic-messages' + ? 'bedrockAnthropicEndpointHelp' + : 'bedrockOpenAiEndpointHelp'; + } + return undefined; +} + /** Preview the selected protocol when adding, or the default model's protocol when editing. */ export function providerRequestUrlPreview( providerType: ProviderType, @@ -63,7 +80,7 @@ export function providerRequestUrlPreview( apiProtocol: ModelApiProtocol = 'openai-chat', ): string | null { if ( - !['custom', 'azure-foundry', 'amazon-bedrock-api-key'].includes(providerType) || + !providerSupportsApiProtocolSelection(providerType) || apiProtocol === 'anthropic-messages' ) { return null; diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index 321338e340b..d301244209d 100644 --- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts @@ -274,6 +274,7 @@ const zhCopy = { shared: { requestUrlLabel: '请求地址:', azureFoundryEndpointHelp: '使用以 /openai/v1 结尾的已部署模型终结点。模型 ID 请填写部署名称;模型列表可能只包含基础模型。', + azureFoundryAnthropicEndpointHelp: 'Anthropic Messages 请使用 https://.services.ai.azure.com/anthropic。模型 ID 请填写部署名称。', bedrockOpenAiEndpointHelp: 'Chat Completions 或 Responses 请使用 https://bedrock-runtime.{region}.amazonaws.com/openai/v1。若未列出,请手动输入 Bedrock 模型 ID。', bedrockAnthropicEndpointHelp: 'Anthropic Messages 请使用 https://bedrock-runtime.{region}.amazonaws.com/anthropic。若未列出,请手动输入 Bedrock 模型 ID。', connectionStale: '连接状态已更新,请刷新列表后再删除。', @@ -490,6 +491,7 @@ const zhTwCopy = { shared: { requestUrlLabel: '請求地址:', azureFoundryEndpointHelp: '請使用以 /openai/v1 結尾的已部署模型端點。模型 ID 請填寫部署名稱;模型清單可能只包含基礎模型。', + azureFoundryAnthropicEndpointHelp: 'Anthropic Messages 請使用 https://.services.ai.azure.com/anthropic。模型 ID 請填寫部署名稱。', bedrockOpenAiEndpointHelp: 'Chat Completions 或 Responses 請使用 https://bedrock-runtime.{region}.amazonaws.com/openai/v1。若未列出,請手動輸入 Bedrock 模型 ID。', bedrockAnthropicEndpointHelp: 'Anthropic Messages 請使用 https://bedrock-runtime.{region}.amazonaws.com/anthropic。若未列出,請手動輸入 Bedrock 模型 ID。', connectionStale: '連線狀態已更新,請重新整理清單後再刪除。', @@ -705,6 +707,7 @@ const enCopy: ProviderSettingsCopy = { shared: { requestUrlLabel: 'Request URL:', azureFoundryEndpointHelp: 'Use the deployed model endpoint ending in /openai/v1. Enter the deployment name as the model ID; the models list may only contain base models.', + azureFoundryAnthropicEndpointHelp: 'For Anthropic Messages, use https://.services.ai.azure.com/anthropic. Enter the deployment name as the model ID.', bedrockOpenAiEndpointHelp: 'For Chat Completions or Responses, use https://bedrock-runtime.{region}.amazonaws.com/openai/v1. Enter the Bedrock model ID manually if it is not listed.', bedrockAnthropicEndpointHelp: 'For Anthropic Messages, use https://bedrock-runtime.{region}.amazonaws.com/anthropic. Enter the Bedrock model ID manually if it is not listed.', connectionStale: 'The connection changed while deleting. Refresh the list and try again.', diff --git a/apps/desktop/src/renderer/features/connection-settings/testing.ts b/apps/desktop/src/renderer/features/connection-settings/testing.ts index 444398e2da5..d77baf0f6b0 100644 --- a/apps/desktop/src/renderer/features/connection-settings/testing.ts +++ b/apps/desktop/src/renderer/features/connection-settings/testing.ts @@ -17,4 +17,4 @@ * under the License. */ -export { providerRequestUrlPreview } from './provider-endpoint-field.js'; +export { providerEndpointHelpKey, providerRequestUrlPreview } from './provider-endpoint-field.js'; diff --git a/apps/desktop/src/renderer/settings/provider-add-form.tsx b/apps/desktop/src/renderer/settings/provider-add-form.tsx index 0df0b5e091e..fe9cfb871ca 100644 --- a/apps/desktop/src/renderer/settings/provider-add-form.tsx +++ b/apps/desktop/src/renderer/settings/provider-add-form.tsx @@ -24,6 +24,7 @@ import { MODEL_API_PROTOCOLS, PROVIDER_REGISTRY, deriveConnectionSlug, + providerSupportsApiProtocolSelection, } from '@maka/core/llm-connections'; import { providerAuthRequiresSecret, @@ -132,8 +133,7 @@ export function AddProviderForm(props: { }>({ baseUrl: defaults.baseUrl, defaultApiProtocol: 'openai-chat' }); const { baseUrl, defaultApiProtocol } = endpoint; const isCustom = props.providerType === 'custom'; - const canChooseApiProtocol = isCustom || props.providerType === 'azure-foundry' || - props.providerType === 'amazon-bedrock-api-key'; + const canChooseApiProtocol = providerSupportsApiProtocolSelection(props.providerType); const [cloudflareAccountId, setCloudflareAccountId] = useState(''); const [apiKey, setApiKey] = useState(''); const [defaultModel, setDefaultModel] = useState(recommendedDefaultModel); diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index a9f38ef44ce..78b6b528b8a 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -265,8 +265,8 @@ export const PROVIDER_DISPLAY_COPY = { en: { name: 'Azure AI Foundry', description: 'Connect a deployed OpenAI-compatible model with its Foundry endpoint and API key.', badge: 'API' }, }, 'amazon-bedrock-api-key': { - 'zh-CN': { name: 'Amazon Bedrock', description: '通过 Bedrock 的 OpenAI 兼容 API、区域终结点和 Bedrock API 密钥连接模型。', badge: 'API' }, - 'zh-TW': { name: 'Amazon Bedrock', description: '透過 Bedrock 的 OpenAI 相容 API、區域端點與 Bedrock API 金鑰連線模型。', badge: 'API' }, + 'zh-CN': { name: 'Amazon Bedrock(API 密钥)', description: '使用 Bedrock API 密钥和区域 OpenAI 兼容端点连接模型;可为 Responses 或 Anthropic Messages 模型选择对应协议。', badge: 'API' }, + 'zh-TW': { name: 'Amazon Bedrock(API 金鑰)', description: '使用 Bedrock API 金鑰與區域 OpenAI 相容端點連線模型;可為 Responses 或 Anthropic Messages 模型選擇對應協定。', badge: 'API' }, en: { name: 'Amazon Bedrock (API key)', description: 'Use a Bedrock API key with its regional OpenAI-compatible endpoint and select Responses or Anthropic Messages for models that require them.', badge: 'API' }, }, huggingface: { diff --git a/packages/core/src/__tests__/provider-catalog-contract.test.ts b/packages/core/src/__tests__/provider-catalog-contract.test.ts index 544cabf8731..6b3e7acaf7f 100644 --- a/packages/core/src/__tests__/provider-catalog-contract.test.ts +++ b/packages/core/src/__tests__/provider-catalog-contract.test.ts @@ -31,6 +31,8 @@ import { import { CATALOG_PROVIDER_TYPES, PROVIDER_REGISTRY, + providerSupportsApiProtocolSelection, + providerSupportsDefaultApiProtocol, isRetiredProvider, providerAcceptsOutputTokenLimit, providerFallbackModelIds, @@ -66,6 +68,21 @@ describe('Moonshot provider regions', () => { }); describe('provider catalog contract — structural invariants over CATALOG_PROVIDER_TYPES', () => { + it('derives protocol picker availability from endpoint-owned registry entries', () => { + assert.equal(providerSupportsApiProtocolSelection('custom'), true); + assert.equal(providerSupportsApiProtocolSelection('azure-foundry'), true); + assert.equal(providerSupportsApiProtocolSelection('amazon-bedrock-api-key'), true); + assert.equal(providerSupportsApiProtocolSelection('openrouter'), false); + assert.equal(providerSupportsApiProtocolSelection('opencode'), false); + assert.equal(providerSupportsDefaultApiProtocol('azure-foundry', 'openai-chat'), true); + assert.equal(providerSupportsDefaultApiProtocol('azure-foundry', 'openai-responses'), true); + assert.equal( + providerSupportsDefaultApiProtocol('amazon-bedrock-api-key', 'anthropic-messages'), + true, + ); + assert.equal(providerSupportsDefaultApiProtocol('openrouter', 'openai-chat'), false); + }); + it('marks tenant- or region-scoped API providers as user-endpoint connections', () => { for (const type of ['azure-foundry', 'amazon-bedrock-api-key'] as const) { const definition = PROVIDER_REGISTRY[type]; @@ -94,7 +111,7 @@ describe('provider catalog contract — structural invariants over CATALOG_PROVI }); assert.deepEqual(definition.protocolAdapters?.['anthropic-messages'], { kind: 'anthropic', - auth: 'api-key', + auth: type === 'amazon-bedrock-api-key' ? 'bearer' : 'api-key', normalizeBaseUrl: true, }); } diff --git a/packages/core/src/llm-connections.ts b/packages/core/src/llm-connections.ts index 04833d5d656..6d7c6af1c75 100644 --- a/packages/core/src/llm-connections.ts +++ b/packages/core/src/llm-connections.ts @@ -45,6 +45,8 @@ import { providerDefaultsOf, providerFallbackModelIds, providerMenuLabel, + providerSupportsApiProtocolSelection, + providerSupportsDefaultApiProtocol, type ApplyPatchProtocol, type ModelApiProtocol, type OpenResponsesCompatibilityProfile, @@ -67,6 +69,8 @@ export { providerDefaultsOf, providerFallbackModelIds, providerMenuLabel, + providerSupportsApiProtocolSelection, + providerSupportsDefaultApiProtocol, }; export type { ApplyPatchProtocol, diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index 3090589a790..9c312e419d8 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -1560,7 +1560,7 @@ const providerRegistry = { apiProtocol: 'openai-responses', responses: { adapter: 'openai', reasoningReplay: 'encrypted-content' }, }, - 'anthropic-messages': { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, + 'anthropic-messages': { kind: 'anthropic', auth: 'bearer', normalizeBaseUrl: true }, }, // Bedrock's OpenAI-compatible endpoint and model-list availability vary // by endpoint family and region. A custom endpoint remains user supplied. @@ -1732,6 +1732,31 @@ export function providerDefaultsOf(providerType: string): ProviderDefaults | und : undefined; } +/** Whether a provider's registry definition permits the user to choose a default API protocol. */ +export function providerSupportsApiProtocolSelection(providerType: ProviderType): boolean { + if (providerType === 'custom') return true; + const defaults = providerDefaultsOf(providerType); + return ( + defaults?.category === 'custom' && + defaults.runtimeAdapter.kind === 'openai-compatible' && + defaults.runtimeAdapter.requireBaseUrl === true && + Object.keys(defaults.protocolAdapters ?? {}).length > 0 + ); +} + +/** Whether the registry can execute a selected default API protocol for this provider. */ +export function providerSupportsDefaultApiProtocol( + providerType: ProviderType, + protocol: ModelApiProtocol, +): boolean { + const defaults = providerDefaultsOf(providerType); + if (!defaults || !providerSupportsApiProtocolSelection(providerType)) return false; + if (providerType === 'custom') return isModelApiProtocol(protocol); + return protocol === 'openai-chat' + ? defaults.runtimeAdapter.kind === 'openai-compatible' + : defaults.protocolAdapters?.[protocol] !== undefined; +} + /** * The models a provider offers with no live list to go on: the baseline it * ships. This is the only reader of diff --git a/packages/core/src/runtime-policy/connection-catalog-codec.ts b/packages/core/src/runtime-policy/connection-catalog-codec.ts index f5fd3b33b09..cdea654e3b9 100644 --- a/packages/core/src/runtime-policy/connection-catalog-codec.ts +++ b/packages/core/src/runtime-policy/connection-catalog-codec.ts @@ -19,6 +19,7 @@ import { isModelApiProtocol, + providerSupportsDefaultApiProtocol, isModelModality, effectiveBaseUrl, PROVIDER_REGISTRY, @@ -400,17 +401,8 @@ export function decodeDefaultApiProtocol( value: unknown, providerType: ProviderType, ): ModelApiProtocol | undefined { - const canChooseProtocol = - providerType === 'custom' || - providerType === 'azure-foundry' || - providerType === 'amazon-bedrock-api-key'; const supportsProtocol = (protocol: ModelApiProtocol): boolean => - providerType === 'custom' - ? isModelApiProtocol(protocol) - : canChooseProtocol && - (protocol === 'openai-chat' - ? PROVIDER_REGISTRY[providerType].runtimeAdapter.kind === 'openai-compatible' - : PROVIDER_REGISTRY[providerType].protocolAdapters?.[protocol] !== undefined); + providerSupportsDefaultApiProtocol(providerType, protocol); if (value === undefined) { if (providerType === 'custom') { throw domainError('custom connection default API protocol is invalid'); From ba6cc699c0b9dc216a9fb7e9026c7861220c50c0 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sat, 10 Oct 2026 13:24:22 +0530 Subject: [PATCH 10/14] fix(providers): use documented Bedrock API-key header --- packages/core/src/__tests__/provider-catalog-contract.test.ts | 4 +++- packages/core/src/provider-registry.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__tests__/provider-catalog-contract.test.ts b/packages/core/src/__tests__/provider-catalog-contract.test.ts index 6b3e7acaf7f..640ba578912 100644 --- a/packages/core/src/__tests__/provider-catalog-contract.test.ts +++ b/packages/core/src/__tests__/provider-catalog-contract.test.ts @@ -111,7 +111,9 @@ describe('provider catalog contract — structural invariants over CATALOG_PROVI }); assert.deepEqual(definition.protocolAdapters?.['anthropic-messages'], { kind: 'anthropic', - auth: type === 'amazon-bedrock-api-key' ? 'bearer' : 'api-key', + // Bedrock's documented Anthropic endpoint accepts the API key in + // x-api-key, as does the Azure Foundry endpoint. + auth: 'api-key', normalizeBaseUrl: true, }); } diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts index 9c312e419d8..fe21acbb910 100644 --- a/packages/core/src/provider-registry.ts +++ b/packages/core/src/provider-registry.ts @@ -1560,7 +1560,7 @@ const providerRegistry = { apiProtocol: 'openai-responses', responses: { adapter: 'openai', reasoningReplay: 'encrypted-content' }, }, - 'anthropic-messages': { kind: 'anthropic', auth: 'bearer', normalizeBaseUrl: true }, + 'anthropic-messages': { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, }, // Bedrock's OpenAI-compatible endpoint and model-list availability vary // by endpoint family and region. A custom endpoint remains user supplied. From 5ac8b20adfb3e8da2a558fdb35aefb37cadb972b Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sat, 10 Oct 2026 14:51:16 +0530 Subject: [PATCH 11/14] test(desktop): tolerate subpixel usage table measurement --- apps/desktop/stories/settings/settings-pages.stories.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/stories/settings/settings-pages.stories.tsx b/apps/desktop/stories/settings/settings-pages.stories.tsx index 34262a0e521..4001757fb49 100644 --- a/apps/desktop/stories/settings/settings-pages.stories.tsx +++ b/apps/desktop/stories/settings/settings-pages.stories.tsx @@ -2832,7 +2832,10 @@ export const UsageLongTail: Story = { const requiredWidth = timeRange.getBoundingClientRect().width + Number.parseFloat(timeCellStyle.paddingLeft) + Number.parseFloat(timeCellStyle.paddingRight); - expect(requiredWidth).toBeLessThanOrEqual(timeCell!.clientWidth); + // Chromium measures text on fractional pixels while clientWidth is an + // integer. The one-pixel allowance preserves the no-clipping check + // without making the assertion depend on font rasterisation rounding. + expect(requiredWidth).toBeLessThanOrEqual(timeCell!.clientWidth + 1); const longTarget = 'anthropic/claude-sonnet-4-5-20250929-preview-extended-thinking'; const targetCellText = within(table).getByText(longTarget); From 3eedada79a2dfed5e67309612c6398a2c496947f Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sun, 11 Oct 2026 22:02:30 +0530 Subject: [PATCH 12/14] docs(providers): clarify API-key-only cloud entries Generated-by: Codex --- .../src/renderer/settings/provider-display-copy.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/renderer/settings/provider-display-copy.ts b/apps/desktop/src/renderer/settings/provider-display-copy.ts index 78b6b528b8a..ab46190dd1e 100644 --- a/apps/desktop/src/renderer/settings/provider-display-copy.ts +++ b/apps/desktop/src/renderer/settings/provider-display-copy.ts @@ -260,14 +260,14 @@ export const PROVIDER_DISPLAY_COPY = { en: { name: 'Cloudflare Workers AI', description: 'Cloudflare-hosted models over the account-scoped API.', badge: 'API' }, }, 'azure-foundry': { - 'zh-CN': { name: 'Azure AI Foundry', description: '使用 Foundry 终结点和 API 密钥连接已部署的 OpenAI 兼容模型。', badge: 'API' }, - 'zh-TW': { name: 'Azure AI Foundry', description: '使用 Foundry 端點與 API 金鑰連線已部署的 OpenAI 相容模型。', badge: 'API' }, - en: { name: 'Azure AI Foundry', description: 'Connect a deployed OpenAI-compatible model with its Foundry endpoint and API key.', badge: 'API' }, + 'zh-CN': { name: 'Azure AI Foundry(API 密钥)', description: '仅支持使用 Foundry 终结点和 API 密钥连接模型。暂不支持 Entra ID;见 #6072。', badge: 'API' }, + 'zh-TW': { name: 'Azure AI Foundry(API 金鑰)', description: '僅支援使用 Foundry 端點與 API 金鑰連線模型。暫不支援 Entra ID;見 #6072。', badge: 'API' }, + en: { name: 'Azure AI Foundry (API key)', description: 'Connect with a Foundry endpoint and API key. Entra ID is not supported yet; see #6072.', badge: 'API' }, }, 'amazon-bedrock-api-key': { - 'zh-CN': { name: 'Amazon Bedrock(API 密钥)', description: '使用 Bedrock API 密钥和区域 OpenAI 兼容端点连接模型;可为 Responses 或 Anthropic Messages 模型选择对应协议。', badge: 'API' }, - 'zh-TW': { name: 'Amazon Bedrock(API 金鑰)', description: '使用 Bedrock API 金鑰與區域 OpenAI 相容端點連線模型;可為 Responses 或 Anthropic Messages 模型選擇對應協定。', badge: 'API' }, - en: { name: 'Amazon Bedrock (API key)', description: 'Use a Bedrock API key with its regional OpenAI-compatible endpoint and select Responses or Anthropic Messages for models that require them.', badge: 'API' }, + 'zh-CN': { name: 'Amazon Bedrock(API 密钥)', description: '仅支持 Bedrock API 密钥;可选择 Chat Completions、Responses 或 Anthropic Messages 协议。暂不支持 SigV4 或 IAM Identity Center;见 #6072。', badge: 'API' }, + 'zh-TW': { name: 'Amazon Bedrock(API 金鑰)', description: '僅支援 Bedrock API 金鑰;可選擇 Chat Completions、Responses 或 Anthropic Messages 協定。暫不支援 SigV4 或 IAM Identity Center;見 #6072。', badge: 'API' }, + en: { name: 'Amazon Bedrock (API key)', description: 'API key only. Choose Chat Completions, Responses or Anthropic Messages; SigV4 and IAM Identity Center are not supported yet (#6072).', badge: 'API' }, }, huggingface: { 'zh-CN': { name: 'Hugging Face', description: 'Inference Providers 路由,聚合多家托管模型', badge: '路由' }, From 7b409a6787ee3fb324c821d99197cb44caba85a2 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sun, 11 Oct 2026 22:06:11 +0530 Subject: [PATCH 13/14] fix(providers): gate new catalog types by Host epoch Generated-by: Codex --- .../__tests__/runtime-host-onboarding.test.ts | 19 +++++ packages/cli/src/onboarding-catalog.ts | 8 +- .../__tests__/handshake-compatibility.test.ts | 74 ++++++++++--------- packages/runtime-host/src/protocol/index.ts | 5 +- 4 files changed, 67 insertions(+), 39 deletions(-) diff --git a/packages/cli/src/__tests__/runtime-host-onboarding.test.ts b/packages/cli/src/__tests__/runtime-host-onboarding.test.ts index 2755642f160..15ed02a256b 100644 --- a/packages/cli/src/__tests__/runtime-host-onboarding.test.ts +++ b/packages/cli/src/__tests__/runtime-host-onboarding.test.ts @@ -1245,6 +1245,25 @@ describe('projectProviders', () => { }); }); + test('CLI onboarding offers only supported protocols for cloud API-key entries', () => { + for (const providerType of ['azure-foundry', 'amazon-bedrock-api-key'] as const) { + const entries = listApiKeyOnboardableProviders().filter( + (provider) => provider.providerType === providerType, + ); + assert.deepEqual( + entries.map((entry) => entry.defaultApiProtocol), + ['openai-chat', 'openai-responses', 'anthropic-messages'], + ); + for (const entry of entries) { + assert.deepEqual(onboardingCreateTarget(entry), { + kind: 'create', + providerType, + defaultApiProtocol: entry.defaultApiProtocol, + }); + } + } + }); + test('several non-canonical connections remain independently editable', () => { const entries = projectProviders( catalog([relay, { ...relay, connectionId: 'relay-2-id', slug: 'my-relay-2' }]), diff --git a/packages/cli/src/onboarding-catalog.ts b/packages/cli/src/onboarding-catalog.ts index 11168c54b9b..cf6f6fd05a2 100644 --- a/packages/cli/src/onboarding-catalog.ts +++ b/packages/cli/src/onboarding-catalog.ts @@ -23,6 +23,8 @@ import { MODEL_API_PROTOCOLS, PROVIDER_REGISTRY, providerAuthSupportsApiKey, + providerSupportsApiProtocolSelection, + providerSupportsDefaultApiProtocol, } from '@maka/core/llm-connections'; import type { ConnectionOnboardingTarget } from '@maka/core/runtime-policy'; import type { OnboardableProvider } from './pi-tui-contracts.js'; @@ -47,8 +49,10 @@ export function listApiKeyOnboardableProviders(): OnboardableProvider[] { requiresBaseUrl: !definition.baseUrl, setupMethod: 'api_key' as const, }; - if (providerType !== 'custom') return [entry]; - return MODEL_API_PROTOCOLS.map((defaultApiProtocol) => ({ + if (!providerSupportsApiProtocolSelection(providerType)) return [entry]; + return MODEL_API_PROTOCOLS.filter((protocol) => + providerSupportsDefaultApiProtocol(providerType, protocol), + ).map((defaultApiProtocol) => ({ ...entry, defaultApiProtocol, label: `${definition.label} (${MODEL_API_PROTOCOL_LABELS[defaultApiProtocol]})`, diff --git a/packages/runtime-host/src/__tests__/handshake-compatibility.test.ts b/packages/runtime-host/src/__tests__/handshake-compatibility.test.ts index fbb9f69c82e..3bda72bd600 100644 --- a/packages/runtime-host/src/__tests__/handshake-compatibility.test.ts +++ b/packages/runtime-host/src/__tests__/handshake-compatibility.test.ts @@ -139,42 +139,44 @@ test('process identity query failure does not consume or prevent the incompatibl ); }); -test('rejects an epoch-39 Host before any domain command', async () => { - let admittedRequest: RequestFrame | undefined; - await withForgedHandshakePeer( - async (transport, hostEpoch, rootId) => { - const hello = decodeClientFrame(await transport.read(2_000)); - assert.ok('kind' in hello && hello.kind === 'hello'); - await writeProtocolFrame(transport, { - kind: 'accepted', - rootId, - hostEpoch, - connectionId: 'forged-epoch-connection', - selectedProtocol: RUNTIME_HOST_PROTOCOL_VERSION, - compatibilityEpoch: 39, - compositionId: 'maka.interactive', - compositionRevision: '1', - state: 'ready', - }); - try { - const next = decodeClientFrame(await transport.read(1_000)); - if (!('kind' in next)) admittedRequest = next; - } catch (error) { - assert.ok( - error instanceof RuntimeHostTransportError && - (error.code === 'closed' || error.code === 'read_eof'), - ); - } - }, - async (result) => { - assert.equal(result.kind, 'unavailable'); - if (result.kind === 'unavailable') { - assert.equal(result.reason, 'handshake_failed'); - } - }, - ); - assert.equal(admittedRequest, undefined); -}); +for (const olderEpoch of [39, 217]) { + test(`rejects an epoch-${olderEpoch} Host before any catalog command`, async () => { + let admittedRequest: RequestFrame | undefined; + await withForgedHandshakePeer( + async (transport, hostEpoch, rootId) => { + const hello = decodeClientFrame(await transport.read(2_000)); + assert.ok('kind' in hello && hello.kind === 'hello'); + await writeProtocolFrame(transport, { + kind: 'accepted', + rootId, + hostEpoch, + connectionId: 'forged-epoch-connection', + selectedProtocol: RUNTIME_HOST_PROTOCOL_VERSION, + compatibilityEpoch: olderEpoch, + compositionId: 'maka.interactive', + compositionRevision: '1', + state: 'ready', + }); + try { + const next = decodeClientFrame(await transport.read(1_000)); + if (!('kind' in next)) admittedRequest = next; + } catch (error) { + assert.ok( + error instanceof RuntimeHostTransportError && + (error.code === 'closed' || error.code === 'read_eof'), + ); + } + }, + async (result) => { + assert.equal(result.kind, 'unavailable'); + if (result.kind === 'unavailable') { + assert.equal(result.reason, 'handshake_failed'); + } + }, + ); + assert.equal(admittedRequest, undefined); + }); +} test('records a registration root mismatch before connecting the endpoint', async () => { await withForgedHandshakePeer( diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts index 3a4a71f0444..36b2ee7b8b7 100644 --- a/packages/runtime-host/src/protocol/index.ts +++ b/packages/runtime-host/src/protocol/index.ts @@ -104,7 +104,10 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const; export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const; // Increment when the same protocol version no longer guarantees safe Client-Host // interoperability. Mismatches are rejected before domain commands are admitted. -export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 217 as const; +export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 218 as const; +// 218: Connection catalog pages can include azure-foundry and +// amazon-bedrock-api-key. Epoch-217 clients reject these providerType values +// with their closed provider registry, so they must not share a Host. // 217: token_usage.lastRequestAnchor may carry completedAt, the provider // request settlement time; transcripts may carry context_compaction_applied // system notes; and session subscriptions forward the same boundary during From df579c381da5140c54f21876157d3266d945e355 Mon Sep 17 00:00:00 2001 From: Garvit Arora Date: Sun, 11 Oct 2026 22:07:18 +0530 Subject: [PATCH 14/14] test(runtime-host): reject epoch-217 catalog clients Generated-by: Codex --- .../__tests__/handshake-compatibility.test.ts | 74 +++++++++---------- .../src/__tests__/host-kernel.test.ts | 8 +- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/packages/runtime-host/src/__tests__/handshake-compatibility.test.ts b/packages/runtime-host/src/__tests__/handshake-compatibility.test.ts index 3bda72bd600..fbb9f69c82e 100644 --- a/packages/runtime-host/src/__tests__/handshake-compatibility.test.ts +++ b/packages/runtime-host/src/__tests__/handshake-compatibility.test.ts @@ -139,44 +139,42 @@ test('process identity query failure does not consume or prevent the incompatibl ); }); -for (const olderEpoch of [39, 217]) { - test(`rejects an epoch-${olderEpoch} Host before any catalog command`, async () => { - let admittedRequest: RequestFrame | undefined; - await withForgedHandshakePeer( - async (transport, hostEpoch, rootId) => { - const hello = decodeClientFrame(await transport.read(2_000)); - assert.ok('kind' in hello && hello.kind === 'hello'); - await writeProtocolFrame(transport, { - kind: 'accepted', - rootId, - hostEpoch, - connectionId: 'forged-epoch-connection', - selectedProtocol: RUNTIME_HOST_PROTOCOL_VERSION, - compatibilityEpoch: olderEpoch, - compositionId: 'maka.interactive', - compositionRevision: '1', - state: 'ready', - }); - try { - const next = decodeClientFrame(await transport.read(1_000)); - if (!('kind' in next)) admittedRequest = next; - } catch (error) { - assert.ok( - error instanceof RuntimeHostTransportError && - (error.code === 'closed' || error.code === 'read_eof'), - ); - } - }, - async (result) => { - assert.equal(result.kind, 'unavailable'); - if (result.kind === 'unavailable') { - assert.equal(result.reason, 'handshake_failed'); - } - }, - ); - assert.equal(admittedRequest, undefined); - }); -} +test('rejects an epoch-39 Host before any domain command', async () => { + let admittedRequest: RequestFrame | undefined; + await withForgedHandshakePeer( + async (transport, hostEpoch, rootId) => { + const hello = decodeClientFrame(await transport.read(2_000)); + assert.ok('kind' in hello && hello.kind === 'hello'); + await writeProtocolFrame(transport, { + kind: 'accepted', + rootId, + hostEpoch, + connectionId: 'forged-epoch-connection', + selectedProtocol: RUNTIME_HOST_PROTOCOL_VERSION, + compatibilityEpoch: 39, + compositionId: 'maka.interactive', + compositionRevision: '1', + state: 'ready', + }); + try { + const next = decodeClientFrame(await transport.read(1_000)); + if (!('kind' in next)) admittedRequest = next; + } catch (error) { + assert.ok( + error instanceof RuntimeHostTransportError && + (error.code === 'closed' || error.code === 'read_eof'), + ); + } + }, + async (result) => { + assert.equal(result.kind, 'unavailable'); + if (result.kind === 'unavailable') { + assert.equal(result.reason, 'handshake_failed'); + } + }, + ); + assert.equal(admittedRequest, undefined); +}); test('records a registration root mismatch before connecting the endpoint', async () => { await withForgedHandshakePeer( diff --git a/packages/runtime-host/src/__tests__/host-kernel.test.ts b/packages/runtime-host/src/__tests__/host-kernel.test.ts index 1e6a9290c80..4e1b83f642a 100644 --- a/packages/runtime-host/src/__tests__/host-kernel.test.ts +++ b/packages/runtime-host/src/__tests__/host-kernel.test.ts @@ -3146,7 +3146,11 @@ describe('non-serving Runtime Host kernel', () => { }); }); - test('rejects a previous-epoch Client before admitting catalog commands', async () => { + test('rejects an epoch-217 Client before a catalog page can carry API-key provider types', async () => { + // Epoch-217 clients have a closed provider registry without azure-foundry + // and amazon-bedrock-api-key. They must never decode pages from this Host. + const preApiKeyProviderEpoch = 217; + assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > preApiKeyProviderEpoch); await withHostPaths(async (paths) => { const candidate = await startTestRuntimeHostCandidate(paths, { rootPath: paths.root, @@ -3162,7 +3166,7 @@ describe('non-serving Runtime Host kernel', () => { clientInstanceId: 'previous-epoch-client', protocolMin: CURRENT_PROTOCOL.min, protocolMax: CURRENT_PROTOCOL.max, - compatibilityEpoch: RUNTIME_HOST_COMPATIBILITY_EPOCH - 1, + compatibilityEpoch: preApiKeyProviderEpoch, compositionId: 'maka.interactive', }); const response = decodeHostFrame(await transport.read(2_000));