mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
feat(server): add GET /api/v1/providers and /settings/models page (#321)
## Summary Operators had no UI surface to see which LLM providers their Fabro server has configured — provider state was only inferable indirectly via the per-model `configured` flag on `GET /api/v1/models`. This adds a dedicated **Models** settings tab backed by a new providers endpoint. - **`fabro_model::Provider`** — a public projection of the internal `CatalogProvider` that *structurally* excludes credential-bearing fields (`auth`, `extra_headers`, `billing_policy`, `agent_profile`). Reused by the generated API client via progenitor `with_replacement`, mirroring the existing `Model` pattern — no parallel API DTO. - **`GET /api/v1/providers`** — lists catalog providers with effective config and a `configured` status stamped per request from `ready_llm_provider_ids()`. Sorted by the catalog's existing `provider_order`. No write endpoints. - **`/settings/models` web page** — new route + nav entry (`CpuChipIcon`, between Integrations and Security) rendering each provider with model count, default model, configured status, and a "Get API key" link for unconfigured providers. ## Key decisions - Provider sort: reuse catalog `provider_order` (priority desc, id asc) — zero extra code. - `adapter` is hidden in the UI row (noisy for first-party providers); the OpenAPI `adapter` field is pinned to an enum matching the closed `AdapterKind` type. - `configured` reflects credential resolution **at the time of the response**, not a frozen startup snapshot — doc/spec wording corrected to match. ## Testing - `fabro-model`: `From<&CatalogProvider>` + serde `skip_serializing_if` unit tests. - `fabro-api`: `Provider` type-identity + JSON-parity tests, including the required/optional field split. - `fabro-server`: handler tests for configured vs unconfigured providers, exact `model_count`/`default_model` against catalog truth, and credential-omission (asserts internal field names *and* the injected credential value never reach the wire). - OpenAPI route conformance test covers `GET /api/v1/providers`. - `cargo build --workspace`, `fmt --check`, `clippy -D warnings` clean; 935 Rust tests pass; web `tsc` typecheck passes. - Reviewed via a 10-persona `ce:review` (autofix) — no P0/P1 in shipped code; 8 safe fixes applied. Not done: manual UI screenshots — the `apps/fabro-web` build is blocked in this environment by an unrelated missing `@assistant-ui/react` dependency. Run `bun install` in `apps/fabro-web` to verify `/settings/models` manually. ## Post-Deploy Monitoring & Validation - **What to watch:** request logs for `GET /api/v1/providers` — expect `200`s for authenticated users, `401` for unauthenticated. The handler resolves LLM credentials per request via `ready_llm_provider_ids()` (the same path the existing `list_models` handler already uses). - **Healthy signals:** `/settings/models` renders the provider list; `configured` matches each provider's actual credential state; no credential strings appear in any response body or log line. - **Failure signals / rollback trigger:** any provider object in the response containing `auth`, `extra_headers`, or a raw key/token value → roll back immediately (the projection type makes this structurally impossible, but treat any occurrence as P0). 5xx spikes on the new route. - **Validation window / owner:** first 24h after deploy, owned by the deploying engineer. Pre-existing note (not introduced here): credential resolution can refresh OAuth tokens and write the vault as a side effect of this read — shared with `list_models`; flagged for a future caching pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ef70dbc5be
commit
fbe8b50a16
23 changed files with 775 additions and 60 deletions
|
|
@ -34,8 +34,8 @@ export function Row({
|
|||
help,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
help?: string;
|
||||
title: ReactNode;
|
||||
help?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
HumanInTheLoopApi,
|
||||
InsightsApi,
|
||||
InstallApi,
|
||||
ModelsApi,
|
||||
RunInternalsApi,
|
||||
RunInternalsApiAxiosParamCreator,
|
||||
RunOutputsApi,
|
||||
|
|
@ -87,6 +88,11 @@ export const installApi = new InstallApi(
|
|||
"",
|
||||
generatedAxios,
|
||||
);
|
||||
export const modelsApi = new ModelsApi(
|
||||
generatedApiConfiguration,
|
||||
"",
|
||||
generatedAxios,
|
||||
);
|
||||
export const runInternalsApi = new RunInternalsApi(
|
||||
generatedApiConfiguration,
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type {
|
|||
PaginatedRunList,
|
||||
PaginatedRunStageList,
|
||||
PaginatedWorkflowListResponse,
|
||||
ProviderList,
|
||||
RunArtifactListResponse,
|
||||
RunBilling,
|
||||
RunProjection,
|
||||
|
|
@ -36,6 +37,7 @@ import {
|
|||
generatedAxios,
|
||||
humanInTheLoopApi,
|
||||
insightsApi,
|
||||
modelsApi,
|
||||
runInternalsApi,
|
||||
runOutputsApi,
|
||||
runsApi,
|
||||
|
|
@ -365,3 +367,11 @@ export function useServerSettings() {
|
|||
immutableOptions,
|
||||
);
|
||||
}
|
||||
|
||||
export function useProviders() {
|
||||
return useSWR<ProviderList>(
|
||||
queryKeys.providers.list(),
|
||||
() => apiData(() => modelsApi.listProviders()),
|
||||
immutableOptions,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,4 +92,7 @@ export const queryKeys = {
|
|||
settings: {
|
||||
server: () => ["settings", "server"] as const,
|
||||
},
|
||||
providers: {
|
||||
list: () => ["providers", "list"] as const,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import * as InsightsNew from "./routes/insights-new";
|
|||
import * as Settings from "./routes/settings";
|
||||
import * as SettingsGeneral from "./routes/settings-general";
|
||||
import * as SettingsIntegrations from "./routes/settings-integrations";
|
||||
import * as SettingsModels from "./routes/settings-models";
|
||||
import * as SettingsSecurity from "./routes/settings-security";
|
||||
import * as SettingsStorage from "./routes/settings-storage";
|
||||
import * as SettingsLiveEvents from "./routes/settings-live-events";
|
||||
|
|
@ -135,6 +136,7 @@ export const routes: RouteObject[] = [
|
|||
children: [
|
||||
indexRoute(SettingsGeneral),
|
||||
route("integrations", SettingsIntegrations),
|
||||
route("models", SettingsModels),
|
||||
route("security", SettingsSecurity),
|
||||
route("storage", SettingsStorage),
|
||||
route("live-events", SettingsLiveEvents),
|
||||
|
|
|
|||
103
apps/fabro-web/app/routes/settings-models.tsx
Normal file
103
apps/fabro-web/app/routes/settings-models.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import type { Provider } from "@qltysh/fabro-api-client";
|
||||
import { useProviders } from "../lib/queries";
|
||||
import {
|
||||
Badge,
|
||||
Dot,
|
||||
Panel,
|
||||
PanelSkeleton,
|
||||
Row,
|
||||
SettingsPageIntro,
|
||||
plural,
|
||||
} from "../components/settings-panel";
|
||||
|
||||
export function meta() {
|
||||
return [{ title: "Models — Fabro" }];
|
||||
}
|
||||
|
||||
export default function SettingsModels() {
|
||||
const query = useProviders();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<SettingsPageIntro description="LLM providers configured on this Fabro server." />
|
||||
{query.data ? (
|
||||
<ProvidersPanel providers={query.data.data} />
|
||||
) : (
|
||||
<PanelSkeleton />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProvidersPanel({ providers }: { providers: Provider[] }) {
|
||||
return (
|
||||
<Panel title="Providers">
|
||||
{providers.length === 0 ? (
|
||||
<div className="px-4 py-6 text-sm text-fg-muted">
|
||||
No LLM providers in the catalog.
|
||||
</div>
|
||||
) : (
|
||||
providers.map((provider) => (
|
||||
<ProviderRow key={provider.id} provider={provider} />
|
||||
))
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderRow({ provider }: { provider: Provider }) {
|
||||
return (
|
||||
<Row
|
||||
title={
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{provider.display_name}
|
||||
<Badge>{provider.id}</Badge>
|
||||
</span>
|
||||
}
|
||||
help={<ProviderHelp provider={provider} />}
|
||||
>
|
||||
<ProviderStatus provider={provider} />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderHelp({ provider }: { provider: Provider }) {
|
||||
return (
|
||||
<span className="inline-flex flex-wrap items-center gap-x-1.5">
|
||||
<span>
|
||||
{provider.model_count} {plural(provider.model_count, "model", "models")}
|
||||
</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>default {provider.default_model ?? "—"}</span>
|
||||
{provider.base_url ? (
|
||||
<>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span className="font-mono">{provider.base_url}</span>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderStatus({ provider }: { provider: Provider }) {
|
||||
return (
|
||||
<span className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Dot on={provider.configured} />
|
||||
<span className={provider.configured ? "text-fg" : "text-fg-muted"}>
|
||||
{provider.configured ? "Configured" : "Not configured"}
|
||||
</span>
|
||||
</span>
|
||||
{!provider.configured && provider.api_key_url ? (
|
||||
<a
|
||||
href={provider.api_key_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs text-teal-500 hover:underline"
|
||||
>
|
||||
Get API key →
|
||||
</a>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import {
|
|||
BoltIcon,
|
||||
CircleStackIcon,
|
||||
Cog6ToothIcon,
|
||||
CpuChipIcon,
|
||||
PuzzlePieceIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
|
@ -38,6 +39,12 @@ const navItems: NavEntry[] = [
|
|||
icon: PuzzlePieceIcon,
|
||||
match: (p) => p.startsWith("/settings/integrations"),
|
||||
},
|
||||
{
|
||||
name: "Models",
|
||||
href: "/settings/models",
|
||||
icon: CpuChipIcon,
|
||||
match: (p) => p.startsWith("/settings/models"),
|
||||
},
|
||||
{
|
||||
name: "Security",
|
||||
href: "/settings/security",
|
||||
|
|
|
|||
|
|
@ -3864,6 +3864,20 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/providers:
|
||||
get:
|
||||
operationId: listProviders
|
||||
tags: [Models]
|
||||
summary: List Providers
|
||||
description: Returns LLM providers from the catalog with effective config and configured status.
|
||||
responses:
|
||||
"200":
|
||||
description: Provider list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ProviderList"
|
||||
|
||||
# ── Completions ───────────────────────────────────────────────────────
|
||||
|
||||
/api/v1/completions:
|
||||
|
|
@ -5028,6 +5042,69 @@ components:
|
|||
meta:
|
||||
$ref: "#/components/schemas/PaginationMeta"
|
||||
|
||||
ProviderList:
|
||||
description: List of LLM providers from the catalog.
|
||||
type: object
|
||||
required:
|
||||
- data
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Provider"
|
||||
|
||||
Provider:
|
||||
description: An LLM provider from the catalog with effective config and configured status.
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- display_name
|
||||
- adapter
|
||||
- priority
|
||||
- model_count
|
||||
- configured
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/ProviderId"
|
||||
display_name:
|
||||
type: string
|
||||
description: Human-readable provider name.
|
||||
example: "Anthropic"
|
||||
adapter:
|
||||
type: string
|
||||
enum: [anthropic, openai, gemini, openai_compatible]
|
||||
description: Protocol adapter the provider speaks.
|
||||
example: "anthropic"
|
||||
base_url:
|
||||
type: ["string", "null"]
|
||||
description: Operator-set base URL override, if any.
|
||||
api_key_url:
|
||||
type: ["string", "null"]
|
||||
description: URL where an operator can obtain an API key for this provider.
|
||||
priority:
|
||||
type: integer
|
||||
format: int32
|
||||
description: Catalog ordering priority; higher sorts first.
|
||||
aliases:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Alternative identifiers that resolve to this provider.
|
||||
model_count:
|
||||
type: integer
|
||||
format: int32
|
||||
minimum: 0
|
||||
description: Number of catalog models belonging to this provider.
|
||||
default_model:
|
||||
type: ["string", "null"]
|
||||
description: Catalog default model ID for this provider, if any.
|
||||
configured:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether credential material is present for this provider on the
|
||||
server when this response was produced. Does NOT imply requests
|
||||
will succeed.
|
||||
|
||||
ProviderId:
|
||||
description: LLM provider identifier.
|
||||
type: string
|
||||
|
|
|
|||
|
|
@ -366,6 +366,7 @@ fn main() {
|
|||
("ExecOutputTail", "fabro_types::ExecOutputTail", &[]),
|
||||
("ProviderId", "fabro_model::ProviderId", &[]),
|
||||
("Model", "fabro_model::Model", &[]),
|
||||
("Provider", "fabro_model::Provider", &[]),
|
||||
("ModelLimits", "fabro_model::ModelLimits", &[]),
|
||||
(
|
||||
"ReasoningEffortFeature",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ mod generated {
|
|||
pub mod types {
|
||||
pub use fabro_model::{
|
||||
Model, ModelCosts, ModelFeatures, ModelLimits, ModelRef as BillingModelRef, ModelTestMode,
|
||||
ReasoningEffortFeature, Speed as BillingSpeed,
|
||||
Provider, ReasoningEffortFeature, Speed as BillingSpeed,
|
||||
};
|
||||
pub use fabro_types::settings::server::{
|
||||
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
|
||||
|
|
|
|||
68
lib/crates/fabro-api/tests/provider_id_round_trip.rs
Normal file
68
lib/crates/fabro-api/tests/provider_id_round_trip.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
|
||||
use fabro_api::types::Model as ApiModel;
|
||||
use fabro_model::{
|
||||
Model, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffortFeature,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn provider_id_reuses_canonical_model_field_type() {
|
||||
assert_same_type::<ApiModel, Model>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_id_json_matches_openapi_shape_through_model() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(ProviderId::anthropic()).unwrap(),
|
||||
json!("anthropic")
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(ProviderId::openai()).unwrap(),
|
||||
json!("openai")
|
||||
);
|
||||
|
||||
let model = Model {
|
||||
id: "venice-custom".to_string(),
|
||||
provider: ProviderId::new("venice"),
|
||||
family: "venice".to_string(),
|
||||
display_name: "Venice Custom".to_string(),
|
||||
limits: ModelLimits {
|
||||
context_window: 128_000,
|
||||
max_output: None,
|
||||
},
|
||||
training: None,
|
||||
knowledge_cutoff: None,
|
||||
features: ModelFeatures {
|
||||
tools: false,
|
||||
vision: false,
|
||||
reasoning: false,
|
||||
reasoning_effort: ReasoningEffortFeature::None,
|
||||
prompt_cache: false,
|
||||
},
|
||||
costs: ModelCosts {
|
||||
input_cost_per_mtok: None,
|
||||
output_cost_per_mtok: None,
|
||||
cache_input_cost_per_mtok: None,
|
||||
},
|
||||
estimated_output_tps: None,
|
||||
aliases: Vec::new(),
|
||||
default: false,
|
||||
configured: true,
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&model).unwrap();
|
||||
assert_eq!(json["provider"], "venice");
|
||||
let round_trip: ApiModel = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(round_trip.provider, ProviderId::new("venice"));
|
||||
}
|
||||
|
||||
fn assert_same_type<T: 'static, U: 'static>() {
|
||||
assert_eq!(
|
||||
TypeId::of::<T>(),
|
||||
TypeId::of::<U>(),
|
||||
"{} should be the same type as {}",
|
||||
type_name::<T>(),
|
||||
type_name::<U>()
|
||||
);
|
||||
}
|
||||
|
|
@ -1,60 +1,81 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
|
||||
use fabro_api::types::Model as ApiModel;
|
||||
use fabro_model::{
|
||||
Model, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffortFeature,
|
||||
};
|
||||
use serde_json::json;
|
||||
use fabro_api::types::Provider as ApiProvider;
|
||||
use fabro_model::adapter::AdapterKind;
|
||||
use fabro_model::{Provider, ProviderId};
|
||||
|
||||
#[test]
|
||||
fn provider_id_reuses_canonical_model_field_type() {
|
||||
assert_same_type::<ApiModel, Model>();
|
||||
fn provider_reuses_canonical_type() {
|
||||
assert_same_type::<ApiProvider, Provider>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_id_json_matches_openapi_shape_through_model() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(ProviderId::anthropic()).unwrap(),
|
||||
json!("anthropic")
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(ProviderId::openai()).unwrap(),
|
||||
json!("openai")
|
||||
);
|
||||
|
||||
let model = Model {
|
||||
id: "venice-custom".to_string(),
|
||||
provider: ProviderId::new("venice"),
|
||||
family: "venice".to_string(),
|
||||
display_name: "Venice Custom".to_string(),
|
||||
limits: ModelLimits {
|
||||
context_window: 128_000,
|
||||
max_output: None,
|
||||
},
|
||||
training: None,
|
||||
knowledge_cutoff: None,
|
||||
features: ModelFeatures {
|
||||
tools: false,
|
||||
vision: false,
|
||||
reasoning: false,
|
||||
reasoning_effort: ReasoningEffortFeature::None,
|
||||
prompt_cache: false,
|
||||
},
|
||||
costs: ModelCosts {
|
||||
input_cost_per_mtok: None,
|
||||
output_cost_per_mtok: None,
|
||||
cache_input_cost_per_mtok: None,
|
||||
},
|
||||
estimated_output_tps: None,
|
||||
aliases: Vec::new(),
|
||||
default: false,
|
||||
configured: true,
|
||||
fn provider_json_matches_openapi_shape() {
|
||||
let provider = Provider {
|
||||
id: ProviderId::anthropic(),
|
||||
display_name: "Anthropic".to_string(),
|
||||
adapter: AdapterKind::Anthropic,
|
||||
base_url: Some("https://api.anthropic.test/v1".to_string()),
|
||||
api_key_url: Some("https://console.anthropic.com/settings/keys".to_string()),
|
||||
priority: 100,
|
||||
aliases: vec!["claude".to_string()],
|
||||
model_count: 7,
|
||||
default_model: Some("claude-opus-4-7".to_string()),
|
||||
configured: true,
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&model).unwrap();
|
||||
assert_eq!(json["provider"], "venice");
|
||||
let round_trip: ApiModel = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(round_trip.provider, ProviderId::new("venice"));
|
||||
let json = serde_json::to_value(&provider).unwrap();
|
||||
assert_eq!(json["id"], "anthropic");
|
||||
assert_eq!(json["display_name"], "Anthropic");
|
||||
assert_eq!(json["adapter"], "anthropic");
|
||||
assert_eq!(json["base_url"], "https://api.anthropic.test/v1");
|
||||
assert_eq!(
|
||||
json["api_key_url"],
|
||||
"https://console.anthropic.com/settings/keys"
|
||||
);
|
||||
assert_eq!(json["priority"], 100);
|
||||
assert_eq!(json["aliases"], serde_json::json!(["claude"]));
|
||||
assert_eq!(json["model_count"], 7);
|
||||
assert_eq!(json["default_model"], "claude-opus-4-7");
|
||||
assert_eq!(json["configured"], true);
|
||||
|
||||
let round_trip: ApiProvider = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(round_trip, provider);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_omits_optional_fields_when_absent() {
|
||||
// Proves the required/optional split the OpenAPI `Provider` schema
|
||||
// declares: the four `skip_serializing_if` fields drop out entirely, while
|
||||
// the six required fields always serialize.
|
||||
let provider = Provider {
|
||||
id: ProviderId::new("custom"),
|
||||
display_name: "Custom".to_string(),
|
||||
adapter: AdapterKind::OpenAiCompatible,
|
||||
base_url: None,
|
||||
api_key_url: None,
|
||||
priority: 0,
|
||||
aliases: Vec::new(),
|
||||
model_count: 0,
|
||||
default_model: None,
|
||||
configured: false,
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&provider).unwrap();
|
||||
let object = json.as_object().unwrap();
|
||||
assert!(!object.contains_key("base_url"));
|
||||
assert!(!object.contains_key("api_key_url"));
|
||||
assert!(!object.contains_key("aliases"));
|
||||
assert!(!object.contains_key("default_model"));
|
||||
assert!(object.contains_key("id"));
|
||||
assert!(object.contains_key("display_name"));
|
||||
assert!(object.contains_key("adapter"));
|
||||
assert!(object.contains_key("priority"));
|
||||
assert!(object.contains_key("model_count"));
|
||||
assert!(object.contains_key("configured"));
|
||||
|
||||
let round_trip: ApiProvider = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(round_trip, provider);
|
||||
}
|
||||
|
||||
fn assert_same_type<T: 'static, U: 'static>() {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use tracing::warn;
|
|||
use crate::Speed;
|
||||
use crate::adapter::{AdapterKind, AgentProfileKind};
|
||||
use crate::ids::ProviderId;
|
||||
use crate::provider::Provider;
|
||||
use crate::reasoning::ReasoningEffort;
|
||||
use crate::types::{Model, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature};
|
||||
|
||||
|
|
@ -777,6 +778,37 @@ impl Catalog {
|
|||
&self.providers
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn provider_summaries(&self, configured: &HashSet<ProviderId>) -> Vec<Provider> {
|
||||
#[derive(Default)]
|
||||
struct Stats {
|
||||
model_count: u32,
|
||||
default_model: Option<String>,
|
||||
}
|
||||
|
||||
let mut stats_by_provider = HashMap::<ProviderId, Stats>::new();
|
||||
for model in &self.models {
|
||||
let stats = stats_by_provider.entry(model.provider.clone()).or_default();
|
||||
stats.model_count = stats.model_count.saturating_add(1);
|
||||
if model.default {
|
||||
stats.default_model = Some(model.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.providers
|
||||
.iter()
|
||||
.map(|provider| {
|
||||
let stats = stats_by_provider.remove(&provider.id).unwrap_or_default();
|
||||
Provider::from_catalog(
|
||||
provider,
|
||||
stats.model_count,
|
||||
stats.default_model,
|
||||
configured.contains(&provider.id),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn provider(&self, id: &ProviderId) -> Option<&CatalogProvider> {
|
||||
let canonical = self.provider_aliases.get(id.as_str()).unwrap_or(id);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ pub mod catalog;
|
|||
pub mod ids;
|
||||
pub mod model_ref;
|
||||
pub mod model_test;
|
||||
pub mod provider;
|
||||
pub mod reasoning;
|
||||
pub mod types;
|
||||
|
||||
|
|
@ -22,5 +23,6 @@ pub use catalog::{
|
|||
pub use ids::{ModelId, ProviderId};
|
||||
pub use model_ref::ModelHandle;
|
||||
pub use model_test::ModelTestMode;
|
||||
pub use provider::Provider;
|
||||
pub use reasoning::ReasoningEffort;
|
||||
pub use types::{Model, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffortFeature};
|
||||
|
|
|
|||
84
lib/crates/fabro-model/src/provider.rs
Normal file
84
lib/crates/fabro-model/src/provider.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::adapter::AdapterKind;
|
||||
use crate::catalog::CatalogProvider;
|
||||
use crate::ids::ProviderId;
|
||||
|
||||
/// A user-facing LLM provider from the catalog.
|
||||
///
|
||||
/// The public projection of [`CatalogProvider`]. It deliberately omits
|
||||
/// internal-only fields (`auth`, `extra_headers`, `billing_policy`,
|
||||
/// `agent_profile`) so credential material never reaches the wire.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Provider {
|
||||
pub id: ProviderId,
|
||||
pub display_name: String,
|
||||
pub adapter: AdapterKind,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub api_key_url: Option<String>,
|
||||
pub priority: i32,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub aliases: Vec<String>,
|
||||
/// Number of catalog models for this provider. Stamped by the handler.
|
||||
pub model_count: u32,
|
||||
/// Catalog default model ID for this provider, if any. Stamped by the
|
||||
/// handler.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_model: Option<String>,
|
||||
/// True if the server has credential material configured for this provider
|
||||
/// when the response is produced. Always `false` in static catalog data;
|
||||
/// stamped by `GET /providers` per request.
|
||||
#[serde(default)]
|
||||
pub configured: bool,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
#[must_use]
|
||||
pub fn from_catalog(
|
||||
provider: &CatalogProvider,
|
||||
model_count: u32,
|
||||
default_model: Option<String>,
|
||||
configured: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: provider.id.clone(),
|
||||
display_name: provider.display_name.clone(),
|
||||
adapter: provider.adapter,
|
||||
base_url: provider.base_url.clone(),
|
||||
api_key_url: provider.api_key_url.clone(),
|
||||
priority: provider.priority,
|
||||
aliases: provider.aliases.clone(),
|
||||
model_count,
|
||||
default_model,
|
||||
configured,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Provider;
|
||||
use crate::catalog::Catalog;
|
||||
use crate::ids::ProviderId;
|
||||
|
||||
#[test]
|
||||
fn from_catalog_provider_copies_static_fields_and_supplied_runtime_fields() {
|
||||
let catalog = Catalog::builtin();
|
||||
let anthropic = catalog
|
||||
.provider(&ProviderId::anthropic())
|
||||
.expect("builtin catalog must define anthropic");
|
||||
|
||||
let provider =
|
||||
Provider::from_catalog(anthropic, 7, Some("claude-opus-4-7".to_string()), true);
|
||||
|
||||
assert_eq!(provider.id, ProviderId::anthropic());
|
||||
assert_eq!(provider.display_name, anthropic.display_name);
|
||||
assert_eq!(provider.adapter, anthropic.adapter);
|
||||
assert_eq!(provider.priority, anthropic.priority);
|
||||
assert_eq!(provider.model_count, 7);
|
||||
assert_eq!(provider.default_model.as_deref(), Some("claude-opus-4-7"));
|
||||
assert!(provider.configured);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,13 +30,13 @@ pub use fabro_api::types::{
|
|||
DeleteSecretRequest, DiskUsageResponse, DiskUsageRunRow, DiskUsageSummaryRow, ForkRequest,
|
||||
ForkResponse, LinkRunPullRequestRequest, MergeRunPullRequestRequest,
|
||||
MergeRunPullRequestResponse, ModelReference, PaginatedEventList, PaginatedRunList,
|
||||
PaginationMeta, PreflightResponse, PreviewUrlRequest, PreviewUrlResponse, PruneRunEntry,
|
||||
PruneRunsRequest, PruneRunsResponse, RenderWorkflowGraphDirection, RenderWorkflowGraphRequest,
|
||||
RewindRequest, RewindResponse, RunArtifactEntry, RunArtifactListResponse, RunBilling,
|
||||
RunBillingStage, RunBillingTotals, RunError, RunManifest, RunStage, SandboxDetails,
|
||||
SandboxFileEntry, SandboxFileListResponse, SandboxService, SandboxServiceListResponse,
|
||||
SshAccessRequest, SshAccessResponse, StageHandler, StageState, StartRunRequest,
|
||||
SubmitAnswerRequest, SystemFeatures, SystemInfoResponse, SystemRepairRunIssue,
|
||||
PaginationMeta, PreflightResponse, PreviewUrlRequest, PreviewUrlResponse, Provider,
|
||||
ProviderList, PruneRunEntry, PruneRunsRequest, PruneRunsResponse, RenderWorkflowGraphDirection,
|
||||
RenderWorkflowGraphRequest, RewindRequest, RewindResponse, RunArtifactEntry,
|
||||
RunArtifactListResponse, RunBilling, RunBillingStage, RunBillingTotals, RunError, RunManifest,
|
||||
RunStage, SandboxDetails, SandboxFileEntry, SandboxFileListResponse, SandboxService,
|
||||
SandboxServiceListResponse, SshAccessRequest, SshAccessResponse, StageHandler, StageState,
|
||||
StartRunRequest, SubmitAnswerRequest, SystemFeatures, SystemInfoResponse, SystemRepairRunIssue,
|
||||
SystemRepairRunsResponse, SystemRunCounts, TimelineEntryResponse, VncPreviewResponse,
|
||||
WriteBlobResponse,
|
||||
};
|
||||
|
|
@ -714,6 +714,11 @@ impl AppState {
|
|||
resolve_llm_client_from_source(self.llm_source.as_ref(), self.catalog()).await
|
||||
}
|
||||
|
||||
pub(crate) async fn configured_llm_provider_ids(&self) -> Vec<ProviderId> {
|
||||
let catalog = self.catalog();
|
||||
self.llm_source.configured_providers(catalog.as_ref()).await
|
||||
}
|
||||
|
||||
pub(crate) async fn ready_llm_provider_ids(&self) -> Vec<ProviderId> {
|
||||
match self.resolve_llm_client().await {
|
||||
Ok(result) => result.provider_ids(),
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ use std::sync::Arc;
|
|||
|
||||
use super::super::{
|
||||
ApiError, AppState, FromStr, HashSet, IntoResponse, Json, MAX_PAGE_OFFSET, ModelTestMode, Path,
|
||||
ProviderId, Query, RequiredUser, Response, Router, State, StatusCode, auth_issue_message,
|
||||
default_page_limit, error, get, post, run_model_test,
|
||||
ProviderId, ProviderList, Query, RequiredUser, Response, Router, State, StatusCode,
|
||||
auth_issue_message, default_page_limit, error, get, post, run_model_test,
|
||||
};
|
||||
|
||||
pub(super) fn routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/models", get(list_models))
|
||||
.route("/models/{id}/test", post(test_model))
|
||||
.route("/providers", get(list_providers))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
|
|
@ -80,6 +81,18 @@ async fn list_models(
|
|||
.into_response()
|
||||
}
|
||||
|
||||
async fn list_providers(_auth: RequiredUser, State(state): State<Arc<AppState>>) -> Response {
|
||||
let catalog = state.catalog();
|
||||
let configured: HashSet<ProviderId> = state
|
||||
.configured_llm_provider_ids()
|
||||
.await
|
||||
.into_iter()
|
||||
.collect();
|
||||
let data = catalog.provider_summaries(&configured);
|
||||
|
||||
(StatusCode::OK, Json(ProviderList { data })).into_response()
|
||||
}
|
||||
|
||||
async fn test_model(
|
||||
_auth: RequiredUser,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
|
|||
|
|
@ -5086,6 +5086,116 @@ reasoning = false
|
|||
assert_eq!(models[0]["provider"], "acme");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_providers_marks_configured_per_provider_and_omits_secrets() {
|
||||
// Only `ANTHROPIC_API_KEY` is supplied, so anthropic resolves as configured
|
||||
// while every other catalog provider does not.
|
||||
let state = test_app_state_with_env_lookup(
|
||||
default_test_server_settings(),
|
||||
RunLayer::default(),
|
||||
5,
|
||||
|name| (name == EnvVars::ANTHROPIC_API_KEY).then(|| "test-key".to_string()),
|
||||
);
|
||||
let app = crate::test_support::build_test_router(state);
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api("/providers"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(req).await.unwrap();
|
||||
let body = response_json!(response, StatusCode::OK).await;
|
||||
let providers = body["data"].as_array().unwrap();
|
||||
|
||||
assert!(
|
||||
providers.len() >= 2,
|
||||
"builtin catalog should expose multiple providers"
|
||||
);
|
||||
|
||||
let anthropic = providers
|
||||
.iter()
|
||||
.find(|provider| provider["id"] == "anthropic")
|
||||
.expect("anthropic provider should be present");
|
||||
assert_eq!(anthropic["configured"].as_bool(), Some(true));
|
||||
|
||||
// `model_count` and `default_model` must reflect the catalog truth for
|
||||
// this exact provider, not merely be populated.
|
||||
let catalog = Catalog::builtin();
|
||||
let expected_model_count = catalog.list(Some(&ProviderId::anthropic())).len();
|
||||
assert_eq!(
|
||||
anthropic["model_count"].as_u64(),
|
||||
Some(expected_model_count as u64),
|
||||
"anthropic model_count should match the catalog"
|
||||
);
|
||||
let expected_default = catalog
|
||||
.default_for_provider(&ProviderId::anthropic())
|
||||
.expect("anthropic should have a catalog default model");
|
||||
assert_eq!(
|
||||
anthropic["default_model"].as_str(),
|
||||
Some(expected_default.id.as_str()),
|
||||
"anthropic default_model should match the catalog"
|
||||
);
|
||||
|
||||
assert!(
|
||||
providers
|
||||
.iter()
|
||||
.filter(|provider| provider["id"] != "anthropic")
|
||||
.all(|provider| provider["configured"].as_bool() == Some(false)),
|
||||
"providers without supplied credentials should be unconfigured"
|
||||
);
|
||||
|
||||
// Internal-only catalog fields and the injected credential value must
|
||||
// never reach the wire.
|
||||
let serialized = body["data"].to_string();
|
||||
assert!(!serialized.contains("\"auth\""), "leaked `auth`");
|
||||
assert!(
|
||||
!serialized.contains("\"extra_headers\""),
|
||||
"leaked `extra_headers`"
|
||||
);
|
||||
assert!(
|
||||
!serialized.contains("\"billing_policy\""),
|
||||
"leaked `billing_policy`"
|
||||
);
|
||||
assert!(
|
||||
!serialized.contains("\"agent_profile\""),
|
||||
"leaked `agent_profile`"
|
||||
);
|
||||
assert!(
|
||||
!serialized.contains("test-key"),
|
||||
"leaked the injected credential value"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_providers_marks_all_unconfigured_without_credentials() {
|
||||
let state = test_app_state_with_env_lookup(
|
||||
default_test_server_settings(),
|
||||
RunLayer::default(),
|
||||
5,
|
||||
|_| None,
|
||||
);
|
||||
let app = crate::test_support::build_test_router(state);
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api("/providers"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(req).await.unwrap();
|
||||
let body = response_json!(response, StatusCode::OK).await;
|
||||
let providers = body["data"].as_array().unwrap();
|
||||
|
||||
assert!(!providers.is_empty());
|
||||
assert!(
|
||||
providers
|
||||
.iter()
|
||||
.all(|provider| provider["configured"].as_bool() == Some(false)),
|
||||
"no provider should be configured when no credentials are supplied"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_login_github_redirects_to_github() {
|
||||
let source = r#"
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@ models/principal-webhook.ts
|
|||
models/principal-worker.ts
|
||||
models/principal.ts
|
||||
models/project-namespace.ts
|
||||
models/provider-list.ts
|
||||
models/provider.ts
|
||||
models/prune-run-entry.ts
|
||||
models/prune-runs-request.ts
|
||||
models/prune-runs-response.ts
|
||||
|
|
|
|||
69
lib/packages/fabro-api-client/src/api/models-api.ts
generated
69
lib/packages/fabro-api-client/src/api/models-api.ts
generated
|
|
@ -29,6 +29,8 @@ import type { ModelTestMode } from '../models';
|
|||
import type { ModelTestResult } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PaginatedModelList } from '../models';
|
||||
// @ts-ignore
|
||||
import type { ProviderList } from '../models';
|
||||
/**
|
||||
* ModelsApi - axios parameter creator
|
||||
*/
|
||||
|
|
@ -90,6 +92,42 @@ export const ModelsApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns LLM providers from the catalog with effective config and configured status.
|
||||
* @summary List Providers
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listProviders: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/v1/providers`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication SessionCookie required
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Tests a model by sending a simple prompt and reporting pass/fail.
|
||||
* @summary Test Model
|
||||
|
|
@ -160,6 +198,18 @@ export const ModelsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['ModelsApi.listModels']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Returns LLM providers from the catalog with effective config and configured status.
|
||||
* @summary List Providers
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listProviders(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ProviderList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listProviders(options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ModelsApi.listProviders']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Tests a model by sending a simple prompt and reporting pass/fail.
|
||||
* @summary Test Model
|
||||
|
|
@ -196,6 +246,15 @@ export const ModelsApiFactory = function (configuration?: Configuration, basePat
|
|||
listModels(provider?: string, query?: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedModelList> {
|
||||
return localVarFp.listModels(provider, query, pageLimit, pageOffset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns LLM providers from the catalog with effective config and configured status.
|
||||
* @summary List Providers
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listProviders(options?: RawAxiosRequestConfig): AxiosPromise<ProviderList> {
|
||||
return localVarFp.listProviders(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Tests a model by sending a simple prompt and reporting pass/fail.
|
||||
* @summary Test Model
|
||||
|
|
@ -228,6 +287,16 @@ export class ModelsApi extends BaseAPI {
|
|||
return ModelsApiFp(this.configuration).listModels(provider, query, pageLimit, pageOffset, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LLM providers from the catalog with effective config and configured status.
|
||||
* @summary List Providers
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public listProviders(options?: RawAxiosRequestConfig) {
|
||||
return ModelsApiFp(this.configuration).listProviders(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a model by sending a simple prompt and reporting pass/fail.
|
||||
* @summary Test Model
|
||||
|
|
|
|||
|
|
@ -196,6 +196,8 @@ export * from './principal-user';
|
|||
export * from './principal-webhook';
|
||||
export * from './principal-worker';
|
||||
export * from './project-namespace';
|
||||
export * from './provider';
|
||||
export * from './provider-list';
|
||||
export * from './prune-run-entry';
|
||||
export * from './prune-runs-request';
|
||||
export * from './prune-runs-response';
|
||||
|
|
|
|||
26
lib/packages/fabro-api-client/src/models/provider-list.ts
generated
Normal file
26
lib/packages/fabro-api-client/src/models/provider-list.ts
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { Provider } from './provider';
|
||||
|
||||
/**
|
||||
* List of LLM providers from the catalog.
|
||||
*/
|
||||
export interface ProviderList {
|
||||
'data': Array<Provider>;
|
||||
}
|
||||
|
||||
72
lib/packages/fabro-api-client/src/models/provider.ts
generated
Normal file
72
lib/packages/fabro-api-client/src/models/provider.ts
generated
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An LLM provider from the catalog with effective config and configured status.
|
||||
*/
|
||||
export interface Provider {
|
||||
/**
|
||||
* LLM provider identifier.
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
* Human-readable provider name.
|
||||
*/
|
||||
'display_name': string;
|
||||
/**
|
||||
* Protocol adapter the provider speaks.
|
||||
*/
|
||||
'adapter': ProviderAdapterEnum;
|
||||
/**
|
||||
* Operator-set base URL override, if any.
|
||||
*/
|
||||
'base_url'?: string | null;
|
||||
/**
|
||||
* URL where an operator can obtain an API key for this provider.
|
||||
*/
|
||||
'api_key_url'?: string | null;
|
||||
/**
|
||||
* Catalog ordering priority; higher sorts first.
|
||||
*/
|
||||
'priority': number;
|
||||
/**
|
||||
* Alternative identifiers that resolve to this provider.
|
||||
*/
|
||||
'aliases'?: Array<string>;
|
||||
/**
|
||||
* Number of catalog models belonging to this provider.
|
||||
*/
|
||||
'model_count': number;
|
||||
/**
|
||||
* Catalog default model ID for this provider, if any.
|
||||
*/
|
||||
'default_model'?: string | null;
|
||||
/**
|
||||
* Whether credential material is present for this provider on the server when this response was produced. Does NOT imply requests will succeed.
|
||||
*/
|
||||
'configured': boolean;
|
||||
}
|
||||
|
||||
export const ProviderAdapterEnum = {
|
||||
ANTHROPIC: 'anthropic',
|
||||
OPENAI: 'openai',
|
||||
GEMINI: 'gemini',
|
||||
OPENAI_COMPATIBLE: 'openai_compatible'
|
||||
} as const;
|
||||
|
||||
export type ProviderAdapterEnum = typeof ProviderAdapterEnum[keyof typeof ProviderAdapterEnum];
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue