diff --git a/apps/api/src/lib/pricing.test.ts b/apps/api/src/lib/pricing.test.ts
index c98a423..638eaaa 100644
--- a/apps/api/src/lib/pricing.test.ts
+++ b/apps/api/src/lib/pricing.test.ts
@@ -23,6 +23,12 @@ describe("provider pricing", () => {
expect(providers.length).toBeGreaterThanOrEqual(7);
});
+ it("all providers have a valid provenance", () => {
+ for (const provider of providers) {
+ expect(["mock", "fallback", "live", "unknown"]).toContain(provider.provenance);
+ }
+ });
+
it("returns provider-specific prices for search, news, and scrape", () => {
expect(getProviderById("search.basic")?.priceUsd).toBe(0.01);
expect(getProviderById("search.pro")?.priceUsd).toBe(0.02);
@@ -58,6 +64,7 @@ describe("provider catalog baseline", () => {
priceUsd: number;
enabled: boolean;
sourceType: string;
+ provenance: string;
}
// These are the canonical baseline providers the demo and SCF pitch depend on.
@@ -69,21 +76,24 @@ describe("provider catalog baseline", () => {
category: "search",
priceUsd: 0.01,
enabled: true,
- sourceType: "deterministic-fallback"
+ sourceType: "deterministic-fallback",
+ provenance: "mock"
},
{
id: "news.fast",
category: "news",
priceUsd: 0.015,
enabled: true,
- sourceType: "deterministic-fallback"
+ sourceType: "deterministic-fallback",
+ provenance: "mock"
},
{
id: "scrape.page",
category: "scrape",
priceUsd: 0.02,
enabled: true,
- sourceType: "deterministic-fallback"
+ sourceType: "deterministic-fallback",
+ provenance: "mock"
}
];
@@ -101,6 +111,7 @@ describe("provider catalog baseline", () => {
expect(actual!.priceUsd, `${rowLabel} priceUsd mismatch`).toBe(expected.priceUsd);
expect(actual!.enabled, `${rowLabel} enabled mismatch`).toBe(expected.enabled);
expect(actual!.sourceType, `${rowLabel} sourceType mismatch`).toBe(expected.sourceType);
+ expect(actual!.provenance, `${rowLabel} provenance mismatch`).toBe(expected.provenance);
});
}
});
diff --git a/apps/api/src/lib/pricing.ts b/apps/api/src/lib/pricing.ts
index 021f696..229a452 100644
--- a/apps/api/src/lib/pricing.ts
+++ b/apps/api/src/lib/pricing.ts
@@ -10,6 +10,7 @@ export const providers: ProviderDefinition[] = [
latencyEstimateMs: 1500,
qualityScore: 99,
sourceType: "live",
+ provenance: "live",
enabled: true
},
{
@@ -21,6 +22,7 @@ export const providers: ProviderDefinition[] = [
latencyEstimateMs: 700,
qualityScore: 75,
sourceType: "deterministic-fallback",
+ provenance: "mock",
enabled: true
},
{
@@ -32,6 +34,7 @@ export const providers: ProviderDefinition[] = [
latencyEstimateMs: 1100,
qualityScore: 90,
sourceType: "deterministic-fallback",
+ provenance: "mock",
enabled: true
},
{
@@ -43,6 +46,7 @@ export const providers: ProviderDefinition[] = [
latencyEstimateMs: 800,
qualityScore: 72,
sourceType: "deterministic-fallback",
+ provenance: "mock",
enabled: true
},
{
@@ -54,6 +58,7 @@ export const providers: ProviderDefinition[] = [
latencyEstimateMs: 1400,
qualityScore: 93,
sourceType: "deterministic-fallback",
+ provenance: "mock",
enabled: true
},
{
@@ -65,6 +70,7 @@ export const providers: ProviderDefinition[] = [
latencyEstimateMs: 1000,
qualityScore: 70,
sourceType: "deterministic-fallback",
+ provenance: "mock",
enabled: true
},
{
@@ -76,6 +82,7 @@ export const providers: ProviderDefinition[] = [
latencyEstimateMs: 1700,
qualityScore: 95,
sourceType: "deterministic-fallback",
+ provenance: "mock",
enabled: true
}
];
diff --git a/apps/api/src/routes/public.test.ts b/apps/api/src/routes/public.test.ts
index a9262cf..af9f60b 100644
--- a/apps/api/src/routes/public.test.ts
+++ b/apps/api/src/routes/public.test.ts
@@ -202,6 +202,43 @@ describe("public routes", () => {
expect(catalogResponse.body.byCategory.scrape.length).toBeGreaterThan(0);
});
+ it("every provider in catalog and providers endpoint includes provenance", async () => {
+ const app = await createPublicApp();
+
+ const providersResponse = await request(app).get("/api/providers");
+ const catalogResponse = await request(app).get("/api/catalog");
+
+ const allProviders = [
+ ...providersResponse.body.providers,
+ ...catalogResponse.body.providers,
+ ...catalogResponse.body.byCategory.search,
+ ...catalogResponse.body.byCategory.news,
+ ...catalogResponse.body.byCategory.scrape
+ ];
+
+ for (const provider of allProviders) {
+ expect(provider).toHaveProperty("provenance");
+ expect(["mock", "fallback", "live", "unknown"]).toContain(provider.provenance);
+ }
+ });
+
+ it("live provider has provenance live and mock providers have provenance mock", async () => {
+ const app = await createPublicApp();
+
+ const providersResponse = await request(app).get("/api/providers");
+ const providersList = providersResponse.body.providers;
+
+ const live = providersList.find((p: { id: string }) => p.id === "search.live");
+ expect(live).toBeDefined();
+ expect(live.provenance).toBe("live");
+
+ for (const p of providersList) {
+ if (p.id !== "search.live") {
+ expect(p.provenance).toBe("mock");
+ }
+ }
+ });
+
it("returns safe default analytics shape for fresh storage", async () => {
const app = await createPublicApp();
diff --git a/apps/web/src/pages/ControlDeckPage.tsx b/apps/web/src/pages/ControlDeckPage.tsx
index 78144f6..be0f4e1 100644
--- a/apps/web/src/pages/ControlDeckPage.tsx
+++ b/apps/web/src/pages/ControlDeckPage.tsx
@@ -428,6 +428,9 @@ export default function ControlDeckPage() {
{provider.sourceType}
+
+ {provider.provenance}
+
))
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
index c7ecfd1..9429dd7 100644
--- a/apps/web/src/styles.css
+++ b/apps/web/src/styles.css
@@ -699,6 +699,26 @@ body {
padding: 0.2rem 0.45rem;
}
+.provenance-badge.mock {
+ border-color: rgba(250, 204, 21, 0.5);
+ color: #facc15;
+}
+
+.provenance-badge.fallback {
+ border-color: rgba(251, 146, 60, 0.5);
+ color: #fb923c;
+}
+
+.provenance-badge.live {
+ border-color: rgba(74, 222, 128, 0.5);
+ color: #4ade80;
+}
+
+.provenance-badge.unknown {
+ border-color: rgba(148, 163, 184, 0.5);
+ color: #94a3b8;
+}
+
.action-row {
border: 1px dashed rgba(130, 159, 199, 0.34);
border-radius: 14px;
diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts
index 2fe1fa0..6dcb01d 100644
--- a/packages/shared/src/schemas.ts
+++ b/packages/shared/src/schemas.ts
@@ -4,6 +4,8 @@ export const queryModeSchema = z.enum(["search", "news", "scrape"]);
export const providerCategorySchema = queryModeSchema;
+export const provenanceSchema = z.enum(["mock", "fallback", "live", "unknown"]);
+
export const providerSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
@@ -13,6 +15,7 @@ export const providerSchema = z.object({
latencyEstimateMs: z.number().int().positive(),
qualityScore: z.number().min(1).max(100),
sourceType: z.enum(["live", "deterministic-fallback", "unavailable"]),
+ provenance: provenanceSchema,
enabled: z.boolean()
});
diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts
index ecc0687..b542f6b 100644
--- a/packages/shared/src/types.ts
+++ b/packages/shared/src/types.ts
@@ -1,6 +1,7 @@
export type QueryMode = "search" | "news" | "scrape";
export type ProviderCategory = QueryMode;
export type SourceType = "live" | "deterministic-fallback" | "unavailable";
+export type Provenance = "mock" | "fallback" | "live" | "unknown";
export type ExecutionFallbackReason =
| "timeout"
| "circuit-open"
@@ -30,6 +31,7 @@ export interface ProviderDefinition {
latencyEstimateMs: number;
qualityScore: number;
sourceType: SourceType;
+ provenance: Provenance;
enabled: boolean;
}