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
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ test('audit toolbar uses a full-width search row above the select row with a rig
indexTemplate,
/id="audit-filter-search"[^>]*placeholder="Search by request ID, model, provider, path, user path, or error\.\.\."/
);
assert.match(indexTemplate, /id="audit-filter-status"[\s\S]*<option value="504">504<\/option>/);
assert.doesNotMatch(indexTemplate, /id="audit-filter-model"/);
assert.doesNotMatch(indexTemplate, /id="audit-filter-provider"/);
assert.doesNotMatch(indexTemplate, /id="audit-filter-path"/);
Expand Down Expand Up @@ -419,6 +420,7 @@ test('audit entry metadata is rendered as a labeled pill row at the bottom of th
assert.match(auditEntry, /<span class="provider-badge mono" x-text="entry\.requested_model \|\| entry\.model \|\| '-'"><\/span>/);
assert.match(auditEntry, /<span class="provider-badge mono" x-text="'request_id: ' \+ \(entry\.request_id \|\| '-'\)"><\/span>/);
assert.match(auditEntry, /<span class="provider-badge mono" x-show="entry\.client_ip" x-text="'ip: ' \+ entry\.client_ip"><\/span>/);
assert.match(auditEntry, /<span class="provider-badge mono" x-show="workflowFailoverTarget\(entry\)" x-text="'failover: ' \+ workflowFailoverTarget\(entry\)"><\/span>/);

const metadataRule = readCSSRule(css, '.audit-entry-metadata');
assert.match(metadataRule, /display:\s*flex/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ test('workflow editor renders a live preview card from the draft workflow state'
);
assert.match(
chartTemplate,
/{{define "workflow-chart"}}[\s\S]*x-data="\{ workflow: {{\.}} \|\| \{\} \}"[\s\S]*x-effect="workflow = {{\.}} \|\| \{\}"[\s\S]*<span class="workflow-node-label">Auth<\/span>[\s\S]*x-text="workflow\.authNodeSublabel"[\s\S]*x-show="workflow\.showGuardrails"[\s\S]*x-show="workflow\.showCache"[\s\S]*x-text="workflow\.aiLabel"/
/{{define "workflow-chart"}}[\s\S]*x-data="\{ workflow: {{\.}} \|\| \{\} \}"[\s\S]*x-effect="workflow = {{\.}} \|\| \{\}"[\s\S]*<span class="workflow-node-label">Auth<\/span>[\s\S]*x-text="workflow\.authNodeSublabel"[\s\S]*x-show="workflow\.showGuardrails"[\s\S]*x-show="workflow\.showCache"[\s\S]*x-text="workflow\.aiLabel"[\s\S]*x-show="workflow\.showFailover"[\s\S]*x-text="workflow\.failoverTargetLabel"/
);
});

Expand All @@ -228,7 +228,7 @@ test('audit log pipeline binds cache visibility and runtime highlight classes ac

assert.match(
template,
/{{template "workflow-chart" "workflowAuditChart\(entry\)"}}[\s\S]*<div class="workflow-conn" x-show="workflow\.showCache" :class="workflow\.cacheConnClass"><\/div>[\s\S]*<div class="workflow-node workflow-node-feature workflow-node-cache" x-show="workflow\.showCache" :class="workflow\.cacheNodeClass">[\s\S]*x-text="workflow\.cacheStatusLabel"/
/{{template "workflow-chart" "workflowAuditChart\(entry\)"}}[\s\S]*<div class="workflow-conn" x-show="workflow\.showCache" :class="workflow\.cacheConnClass"><\/div>[\s\S]*<div class="workflow-node workflow-node-feature workflow-node-cache" x-show="workflow\.showCache" :class="workflow\.cacheNodeClass">[\s\S]*x-text="workflow\.cacheStatusLabel"[\s\S]*<div class="workflow-conn" x-show="workflow\.showFailover" :class="workflow\.failoverConnClass"><\/div>[\s\S]*<div class="workflow-node workflow-node-feature workflow-node-failover" x-show="workflow\.showFailover" :class="workflow\.failoverNodeClass">[\s\S]*x-text="workflow\.failoverStatusLabel"[\s\S]*x-text="workflow\.failoverTargetLabel"/
);
assert.match(
template,
Expand Down
122 changes: 113 additions & 9 deletions internal/admin/dashboard/static/js/modules/workflows.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,27 @@
return this.workflowNormalizedFeatures(raw);
},

workflowEntryFailover(entry) {
const raw = entry && entry.data && entry.data.failover;
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
return null;
}

const targetModel = String(raw.target_model || raw.targetModel || '').trim() || null;
if (!targetModel) {
return null;
}

return {
targetModel
};
},

workflowFailoverTarget(entry) {
const failover = this.workflowEntryFailover(entry);
return failover && failover.targetModel ? failover.targetModel : null;
},

workflowSourceGuardrails(source) {
const raw = Array.isArray(source && source.workflow_payload && source.workflow_payload.guardrails)
? source.workflow_payload.guardrails
Expand Down Expand Up @@ -1194,6 +1215,11 @@
cacheNodeClass: this.workflowCacheNodeClass(runtime),
cacheConnClass: this.workflowCacheConnClass(runtime),
cacheStatusLabel: this.workflowCacheStatusLabel(runtime),
showFailover: !!features.fallback || this.workflowRuntimeUsedFailover(runtime),
failoverNodeClass: this.workflowFailoverNodeClass(runtime),
failoverConnClass: this.workflowFailoverConnClass(runtime),
failoverStatusLabel: this.workflowFailoverStatusLabel(runtime),
failoverTargetLabel: this.workflowFailoverTargetLabel(runtime),
aiLabel: this.workflowAiLabel(source, runtime),
aiSublabel: this.workflowAiSublabel(source, runtime),
aiConnClass: this.workflowAiConnClass(runtime),
Expand All @@ -1217,8 +1243,17 @@

workflowAuditChart(entry) {
const source = this.auditEntryWorkflow(entry);
const runtime = this.workflowRuntimeFromEntry(entry);
const features = this.workflowEntryFeatures(entry) || this.workflowSourceFeatures(source);
const runtime = this.workflowRuntimeFromEntry(entry, source);
const features = this.workflowEntryFeatures(entry)
|| (source
? this.workflowSourceFeatures(source)
: {
cache: false,
audit: false,
usage: false,
guardrails: false,
fallback: false
});
return this.workflowChartModel(source, runtime, {
entry,
features,
Expand All @@ -1231,6 +1266,7 @@
// runtime shape: {
// cacheHit: bool,
// cacheType: 'exact'|'semantic'|null,
// failoverTarget: string|null,
// provider,
// model,
// statusCode: number|null,
Expand All @@ -1241,6 +1277,10 @@
return !!(runtime && runtime.cacheHit);
},

workflowRuntimeUsedFailover(runtime) {
return !!(runtime && runtime.failoverTarget);
},

workflowShowCacheStep(source, runtime) {
return this.workflowHasCache(source) || this.workflowRuntimeHasCache(runtime);
},
Expand All @@ -1259,6 +1299,22 @@
return 'Hit (Exact)';
},

workflowFailoverNodeClass(runtime) {
return runtime && runtime.failoverTarget ? 'workflow-node-success' : '';
},

workflowFailoverConnClass(runtime) {
return runtime && runtime.failoverTarget ? 'workflow-conn-hit' : '';
},

workflowFailoverStatusLabel(runtime) {
return runtime && runtime.failoverTarget ? 'Redirected' : null;
},

workflowFailoverTargetLabel(runtime) {
return runtime && runtime.failoverTarget ? runtime.failoverTarget : null;
},

workflowAiConnClass(runtime) {
if (!runtime) return '';
if (runtime.cacheHit) return 'workflow-conn-dim';
Expand Down Expand Up @@ -1298,7 +1354,52 @@
return visible && highlightPresent ? 'workflow-node-success' : '';
},

workflowRuntimeFromEntry(entry) {
workflowQualifiedSelectorParts(selector) {
const raw = String(selector || '').trim();
if (!raw) return null;
const slashIndex = raw.indexOf('/');
if (slashIndex <= 0 || slashIndex >= raw.length - 1) {
return null;
}
return {
provider: raw.slice(0, slashIndex),
model: raw.slice(slashIndex + 1)
};
},

workflowPrimaryRouteFromEntry(entry, source) {
const requestedModel = String(entry && (entry.requested_model || entry.model) || '').trim();
const failover = this.workflowEntryFailover(entry);
if (!(failover && failover.targetModel)) {
return {
provider: String(entry && entry.provider || '').trim() || null,
model: requestedModel || null
};
}

const qualifiedRequested = this.workflowQualifiedSelectorParts(requestedModel);
if (qualifiedRequested) {
return qualifiedRequested;
}

const scopeProvider = this.workflowScopeProviderValue(source && source.scope);
const scopeModel = scopeProvider
? String(source && source.scope && source.scope.scope_model || '').trim()
: '';
if (scopeProvider || scopeModel) {
return {
provider: scopeProvider || null,
model: scopeModel || requestedModel || null
};
}

return {
provider: null,
model: requestedModel || null
};
},

workflowRuntimeFromEntry(entry, source) {
if (!entry) return null;
const normalizedCacheType = (() => {
const value = String(entry.cache_type || '').trim().toLowerCase();
Expand All @@ -1313,18 +1414,21 @@
return Number.isFinite(value) ? value : null;
})();
const cacheHit = normalizedCacheType
? true
: (entry.cache_hit !== undefined && entry.cache_hit !== null)
? !!entry.cache_hit
: false;
? true
: (entry.cache_hit !== undefined && entry.cache_hit !== null)
? !!entry.cache_hit
: false;
const failover = this.workflowEntryFailover(entry);
const primaryRoute = this.workflowPrimaryRouteFromEntry(entry, source);
const responseSuccess = Number.isFinite(statusCode) && statusCode >= 200 && statusCode < 300;
const authError = String(entry.error_type || '').trim().toLowerCase() === 'authentication_error';
const authMethod = String(entry.auth_method || '').trim().toLowerCase() || null;
return {
cacheHit,
cacheType: normalizedCacheType || null,
provider: entry.provider || null,
model: entry.requested_model || entry.model || null,
failoverTarget: failover && failover.targetModel ? failover.targetModel : null,
provider: primaryRoute.provider,
model: primaryRoute.model,
statusCode,
responseSuccess,
aiSuccess: responseSuccess && !cacheHit,
Expand Down
Loading
Loading