Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assistant/scripts/sync-llm-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ function projectProvider(entry: ProviderCatalogEntry): Record<string, unknown> {
projected.apiKeyPlaceholder = entry.apiKeyPlaceholder;
if (entry.credentialsGuide !== undefined)
projected.credentialsGuide = entry.credentialsGuide;
if (entry.supportsManagedAuth !== undefined)
projected.supportsManagedAuth = entry.supportsManagedAuth;
projected.defaultModel = entry.defaultModel;
projected.models = entry.models.map(projectModel);
// NOTE: `apiKeyUrl` intentionally omitted — clients use
Expand Down
18 changes: 18 additions & 0 deletions assistant/src/__tests__/llm-catalog-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "bun:test";

import { MANAGED_PROVIDER_META } from "../providers/managed-proxy/constants.js";
import { PROVIDER_CATALOG } from "../providers/model-catalog.js";
import { resolvePricing, resolvePricingForUsage } from "../util/pricing.js";

Expand Down Expand Up @@ -70,6 +71,7 @@ interface ClientCatalogEntry {
envVar?: string;
apiKeyPlaceholder?: string;
credentialsGuide?: ClientCatalogCredentialsGuide;
supportsManagedAuth?: boolean;
defaultModel: string;
models: ClientCatalogModel[];
}
Expand Down Expand Up @@ -122,13 +124,29 @@ describe("LLM catalog parity: daemon vs client", () => {
expect(clientEntry.setupHint).toBe(daemonEntry.setupHint);
expect(clientEntry.envVar).toBe(daemonEntry.envVar);
expect(clientEntry.apiKeyPlaceholder).toBe(daemonEntry.apiKeyPlaceholder);
expect(clientEntry.supportsManagedAuth).toBe(
daemonEntry.supportsManagedAuth,
);
expect(clientEntry.credentialsGuide).toEqual(
daemonEntry.credentialsGuide,
);
expect(clientEntry.defaultModel).toBe(daemonEntry.defaultModel);
}
});

test("supportsManagedAuth mirrors MANAGED_PROVIDER_META", () => {
// The catalog field is derived from MANAGED_PROVIDER_META at build
// time. This test guards against future hand-edits to model-catalog.ts
// that would let the two drift. Adding a provider to MANAGED_PROVIDER_META
// must auto-propagate; flipping `managed: true` to `false` (or vice
// versa) must propagate too.
for (const entry of PROVIDER_CATALOG) {
const expectedSupportsManagedAuth =
MANAGED_PROVIDER_META[entry.id]?.managed === true;
expect(entry.supportsManagedAuth).toBe(expectedSupportsManagedAuth);
}
});

test("each provider's model count matches the daemon catalog", () => {
const json = loadClientCatalog();

Expand Down
16 changes: 16 additions & 0 deletions assistant/src/providers/model-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { MANAGED_PROVIDER_META } from "./managed-proxy/constants.js";

export type LongContextMode =
| "native-model"
| "provider-request-option"
Expand Down Expand Up @@ -86,6 +88,15 @@ export interface ProviderCatalogEntry {
url: string;
linkLabel: string;
};
/**
* Whether this provider supports the `platform` auth type (Vellum-managed
* keys routed through the platform proxy). Derived from
* `MANAGED_PROVIDER_META` at catalog build time so the two stay in lock
* step. Clients use this field to hide the "Platform (managed by Vellum)"
* option from the auth-type dropdown for providers like Fireworks or
* OpenRouter where managed keys are not available.
*/
supportsManagedAuth?: boolean;
}

/**
Expand Down Expand Up @@ -833,6 +844,11 @@ export const PROVIDER_CATALOG: ProviderCatalogEntry[] =
RAW_PROVIDER_CATALOG.map((entry) => ({
...entry,
models: entry.models.map(catalogModel),
// Derive supportsManagedAuth from MANAGED_PROVIDER_META so the catalog
// and the proxy routing table can never drift. Adding a provider to
// MANAGED_PROVIDER_META with `managed: true` automatically opts it into
// the Platform auth-type dropdown in the clients.
supportsManagedAuth: MANAGED_PROVIDER_META[entry.id]?.managed === true,
}));

/** Check if a model ID is in the catalog for a given provider. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1187,17 +1187,23 @@ public final class SettingsStore: ObservableObject {

// MARK: - Provider Capability Helpers

/// Provider IDs that support managed proxy routing (i.e., can be used in managed mode).
/// Mirrors the `MANAGED_PROVIDER_META` table in the backend.
private static let managedCapableProviderIds: Set<String> = ["anthropic", "openai", "gemini"]

/// Provider IDs that support native web search (inference-provider-native).
/// Anthropic and OpenAI pass `useNativeWebSearch` to their providers; others do not.
private static let nativeWebSearchCapableProviderIds: Set<String> = ["anthropic", "openai"]

/// Returns the catalog entries for providers that support managed proxy routing.
/// Source of truth: the `supportsManagedAuth` field on `LLMProviderRegistry`
/// entries, which is derived upstream from `MANAGED_PROVIDER_META` at catalog
/// build time. Reading from the registry (not `providerCatalog`) keeps the
/// answer stable across daemon `model_info` refreshes — the wire-protocol
/// `ProviderCatalogEntry` doesn't carry capability flags.
var managedCapableProviders: [ProviderCatalogEntry] {
providerCatalog.filter { Self.managedCapableProviderIds.contains($0.id) }
let managedIds = Set(
LLMProviderRegistry.providers
.filter { $0.supportsManagedAuth == true }
.map(\.id)
)
return providerCatalog.filter { managedIds.contains($0.id) }
}

/// Returns the catalog entries for providers that support native web search.
Expand All @@ -1206,8 +1212,9 @@ public final class SettingsStore: ObservableObject {
}

/// Whether a given provider supports managed proxy routing.
/// See `managedCapableProviders` for the source-of-truth rationale.
func isManagedCapable(_ provider: String) -> Bool {
Self.managedCapableProviderIds.contains(provider)
LLMProviderRegistry.provider(id: provider)?.supportsManagedAuth == true
}

/// Whether the current inference selection supports native web search.
Expand Down
6 changes: 6 additions & 0 deletions clients/shared/Resources/llm-provider-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"url": "https://console.anthropic.com/settings/keys",
"linkLabel": "Open Anthropic Console"
},
"supportsManagedAuth": true,
"defaultModel": "claude-opus-4-7",
"models": [
{
Expand Down Expand Up @@ -106,6 +107,7 @@
"url": "https://platform.openai.com/api-keys",
"linkLabel": "Open OpenAI Platform"
},
"supportsManagedAuth": true,
"defaultModel": "gpt-5.5",
"models": [
{
Expand Down Expand Up @@ -250,6 +252,7 @@
"url": "https://aistudio.google.com/apikey",
"linkLabel": "Open Google AI Studio"
},
"supportsManagedAuth": true,
"defaultModel": "gemini-2.5-flash",
"models": [
{
Expand Down Expand Up @@ -411,6 +414,7 @@
"url": "https://ollama.com/download",
"linkLabel": "Download Ollama"
},
"supportsManagedAuth": false,
"defaultModel": "llama3.2",
"models": [
{
Expand Down Expand Up @@ -452,6 +456,7 @@
"url": "https://fireworks.ai/account/api-keys",
"linkLabel": "Open Fireworks Dashboard"
},
"supportsManagedAuth": false,
"defaultModel": "accounts/fireworks/models/kimi-k2p5",
"models": [
{
Expand Down Expand Up @@ -485,6 +490,7 @@
"url": "https://openrouter.ai/keys",
"linkLabel": "Open OpenRouter"
},
"supportsManagedAuth": false,
"defaultModel": "x-ai/grok-4.20-beta",
"models": [
{
Expand Down
11 changes: 11 additions & 0 deletions clients/shared/Utilities/LLMProviderRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ public struct LLMProviderEntry: Decodable {
/// Guide for obtaining API credentials from this provider. `nil` for
/// keyless providers.
public let credentialsGuide: LLMCredentialsGuide?
/// Whether this provider supports the `platform` auth type — i.e.
/// Vellum-managed keys routed through the platform proxy. Derived
/// upstream from `MANAGED_PROVIDER_META` in
/// `assistant/src/providers/managed-proxy/constants.ts`. When `false`
/// (or absent in older catalog versions, in which case it defaults to
/// `false`), the auth-type dropdown hides the "Platform (managed by
/// Vellum)" option for this provider — selecting it would have no
/// effect since there's no managed proxy route for the provider.
public let supportsManagedAuth: Bool?
/// The default model ID (must be present in `models`).
public let defaultModel: String
/// All models offered by this provider.
Expand All @@ -162,6 +171,7 @@ public struct LLMProviderEntry: Decodable {
envVar: String?,
apiKeyPlaceholder: String?,
credentialsGuide: LLMCredentialsGuide?,
supportsManagedAuth: Bool? = nil,
defaultModel: String,
models: [LLMModelEntry]
) {
Expand All @@ -173,6 +183,7 @@ public struct LLMProviderEntry: Decodable {
self.envVar = envVar
self.apiKeyPlaceholder = apiKeyPlaceholder
self.credentialsGuide = credentialsGuide
self.supportsManagedAuth = supportsManagedAuth
self.defaultModel = defaultModel
self.models = models
}
Expand Down
6 changes: 6 additions & 0 deletions meta/llm-provider-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"url": "https://console.anthropic.com/settings/keys",
"linkLabel": "Open Anthropic Console"
},
"supportsManagedAuth": true,
"defaultModel": "claude-opus-4-7",
"models": [
{
Expand Down Expand Up @@ -106,6 +107,7 @@
"url": "https://platform.openai.com/api-keys",
"linkLabel": "Open OpenAI Platform"
},
"supportsManagedAuth": true,
"defaultModel": "gpt-5.5",
"models": [
{
Expand Down Expand Up @@ -250,6 +252,7 @@
"url": "https://aistudio.google.com/apikey",
"linkLabel": "Open Google AI Studio"
},
"supportsManagedAuth": true,
"defaultModel": "gemini-2.5-flash",
"models": [
{
Expand Down Expand Up @@ -411,6 +414,7 @@
"url": "https://ollama.com/download",
"linkLabel": "Download Ollama"
},
"supportsManagedAuth": false,
"defaultModel": "llama3.2",
"models": [
{
Expand Down Expand Up @@ -452,6 +456,7 @@
"url": "https://fireworks.ai/account/api-keys",
"linkLabel": "Open Fireworks Dashboard"
},
"supportsManagedAuth": false,
"defaultModel": "accounts/fireworks/models/kimi-k2p5",
"models": [
{
Expand Down Expand Up @@ -485,6 +490,7 @@
"url": "https://openrouter.ai/keys",
"linkLabel": "Open OpenRouter"
},
"supportsManagedAuth": false,
"defaultModel": "x-ai/grok-4.20-beta",
"models": [
{
Expand Down
Loading