- {fillTemplate(variant.baseUrl, placeholderValues)} · {variant.protocol}
+ {serviceProtocols(service.id).join(' · ')}
)}
- {account.endpoint ? (
+ {account.routing?.kind === 'pinned' ? (
- {account.endpoint.baseUrl} · {account.endpoint.protocol}
+ {account.routing.baseUrl} · {account.routing.protocol}
+
+
+ ) : account.routing?.kind === 'catalog' ? (
+
+
+ {account.routing.protocols.join(' · ')}
) : null}
@@ -311,8 +318,8 @@ function bindingStatusLabel(
switch (status.kind) {
case 'unavailable-oauth':
return t('unavailableOauth', { agent: tAgent(status.agent) });
- case 'unavailable-translation-endpoint':
- return t('unavailableTranslationEndpoint');
+ case 'unavailable-endpoint-incomplete':
+ return t('unavailableEndpointIncomplete');
case 'unavailable-protocol':
return t('unavailableProtocol');
case 'bound':
diff --git a/packages/presentation/ui/src/shell/providers/account-master-list.tsx b/packages/presentation/ui/src/shell/providers/account-master-list.tsx
index 101ef3b42..aadef5611 100644
--- a/packages/presentation/ui/src/shell/providers/account-master-list.tsx
+++ b/packages/presentation/ui/src/shell/providers/account-master-list.tsx
@@ -8,14 +8,14 @@ import { ChevronRightIcon, PlusIcon } from 'lucide-react';
import { useState } from 'react';
import { useTranslations } from 'use-intl';
import { ServiceIcon } from '../service-icon';
+import type { ProviderAccountRouting } from './routing';
export interface ProviderAccountListItem {
id: string;
service?: string;
label: string;
serviceLabel?: string;
- endpoint?: string;
- protocol?: string;
+ routing?: ProviderAccountRouting;
credentialType: 'api-key' | 'auth-token' | 'oauth';
auth?: { loggedIn: boolean; email?: string };
boundAgents: AgentKind[];
@@ -61,16 +61,26 @@ export function AccountList({
return t('credentialAuthToken');
};
- const accountDetailLine = (account: ProviderAccountListItem): string => {
+ /** Undefined means the row has no second line — the sole decision, so no separate render gate
+ * can disagree with it. Catalog accounts deliberately show nothing: their wire shapes are an
+ * implementation detail the add flow stopped asking about, and this slot has no label to give
+ * bare protocol names any meaning (the detail pane shows them under `t('protocols')`). */
+ const accountDetailLine = (account: ProviderAccountListItem): string | undefined => {
if (account.auth?.loggedIn === true) return account.auth.email ?? t('loggedIn');
if (account.auth) return t('loggedOut');
- return [account.endpoint, account.protocol].filter(Boolean).join(' · ');
+ return account.routing?.kind === 'pinned'
+ ? `${account.routing.baseUrl} · ${account.routing.protocol}`
+ : undefined;
};
const needle = query.trim().toLowerCase();
const rows = needle
? accounts.filter((account) =>
- [account.label, account.serviceLabel ?? '', account.endpoint ?? '']
+ [
+ account.label,
+ account.serviceLabel ?? '',
+ account.routing?.kind === 'pinned' ? account.routing.baseUrl : '',
+ ]
.join(' ')
.toLowerCase()
.includes(needle),
@@ -114,46 +124,53 @@ export function AccountList({
>
) : null}
- {rows.map((account) => (
-
-
+
+ );
+ })}
{!loading && needle && rows.length === 0 ? (
{t('noMatches')}
diff --git a/packages/presentation/ui/src/shell/providers/routing.ts b/packages/presentation/ui/src/shell/providers/routing.ts
new file mode 100644
index 000000000..42c25a04a
--- /dev/null
+++ b/packages/presentation/ui/src/shell/providers/routing.ts
@@ -0,0 +1,9 @@
+/**
+ * How an account reaches a provider, as one exhaustive axis rather than a set of optional fields
+ * that can contradict each other. An account either pins one endpoint the user named, or leaves
+ * the endpoint to be resolved per agent from the service catalog. Absent covers the accounts that
+ * name no endpoint at all: a CLI login, or a pre-catalog bare key.
+ */
+export type ProviderAccountRouting =
+ | { kind: 'pinned'; baseUrl: string; protocol: string }
+ | { kind: 'catalog'; protocols: string[] };
diff --git a/packages/presentation/ui/src/shell/service-icon.tsx b/packages/presentation/ui/src/shell/service-icon.tsx
index b05459e85..e5f40bafd 100644
--- a/packages/presentation/ui/src/shell/service-icon.tsx
+++ b/packages/presentation/ui/src/shell/service-icon.tsx
@@ -2,6 +2,7 @@
import AnthropicGlyph from '~icons/lobe-icons/anthropic';
import ClaudeColorGlyph from '~icons/lobe-icons/claude-color';
import CloudflareColorGlyph from '~icons/lobe-icons/cloudflare-color';
+import DeepSeekColorGlyph from '~icons/lobe-icons/deepseek-color';
import OpenAiGlyph from '~icons/lobe-icons/openai';
import OpenRouterGlyph from '~icons/lobe-icons/openrouter';
import VercelGlyph from '~icons/lobe-icons/vercel';
@@ -17,9 +18,11 @@ const SERVICE_GLYPHS: Record = {
'anthropic-api': AnthropicGlyph,
'openai-api': OpenAiGlyph,
xai: XaiGlyph,
+ deepseek: DeepSeekColorGlyph,
openrouter: OpenRouterGlyph,
'vercel-gateway': VercelGlyph,
'cloudflare-gateway': CloudflareColorGlyph,
+ 'cloudflare-anthropic': CloudflareColorGlyph,
};
const WHITESPACE = /\s+/;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1658a9cc1..277d2c132 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -798,6 +798,9 @@ importers:
'@linkcode/i18n':
specifier: workspace:*
version: link:../../presentation/i18n
+ '@linkcode/providers':
+ specifier: workspace:*
+ version: link:../../foundation/providers
'@linkcode/schema':
specifier: workspace:*
version: link:../../foundation/schema
@@ -897,6 +900,15 @@ importers:
specifier: 'catalog:'
version: 26.1.1
+ packages/foundation/providers:
+ dependencies:
+ '@linkcode/schema':
+ specifier: workspace:*
+ version: link:../schema
+ foxts:
+ specifier: ^5.8.1
+ version: 5.8.1
+
packages/foundation/schema:
dependencies:
canonicalize:
@@ -1053,6 +1065,9 @@ importers:
'@linkcode/common':
specifier: workspace:*
version: link:../../foundation/common
+ '@linkcode/providers':
+ specifier: workspace:*
+ version: link:../../foundation/providers
'@linkcode/schema':
specifier: workspace:*
version: link:../../foundation/schema
@@ -8360,9 +8375,6 @@ packages:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
- jose@6.2.4:
- resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==}
-
jose@6.2.7:
resolution: {integrity: sha512-hq1OB1bALKfydZNoViyg6hPVGV4i93ny9Op+n4zP5RSf7SCZEXa/TsG2O3IEr7+WlHRTPnpqDmHfMH6qXAD60w==}
@@ -13952,7 +13964,7 @@ snapshots:
express: 5.2.1
express-rate-limit: 8.6.1(express@5.2.1)
hono: 4.12.32
- jose: 6.2.4
+ jose: 6.2.7
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
@@ -18987,8 +18999,6 @@ snapshots:
jiti@2.7.0: {}
- jose@6.2.4: {}
-
jose@6.2.7: {}
joycon@3.1.1: {}
diff --git a/tsconfig.json b/tsconfig.json
index a0d8412af..e8b7e410c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -19,6 +19,7 @@
{ "path": "packages/client/workbench" },
{ "path": "packages/client/workbench/tests" },
{ "path": "packages/foundation/common" },
+ { "path": "packages/foundation/providers" },
{ "path": "packages/foundation/schema" },
{ "path": "packages/foundation/schema/tests" },
{ "path": "packages/foundation/transport" },