Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
3214cdb
feat(gateway): add the fact space and shared stages the families run on
Menci Aug 16, 2026
837215b
Merge branch 'feat/pipeline-core' into feat/pipeline-non-chat
Menci Aug 16, 2026
72baf7e
feat(rerank): serve rerank as a pipeline
Menci Aug 16, 2026
682c89e
feat(embeddings): serve embeddings as a pipeline
Menci Aug 16, 2026
53b9736
feat(gateway): serve the remaining non-chat families as pipelines
Menci Aug 16, 2026
16bc1b2
Merge branch 'feat/pipeline-core' into feat/pipeline-non-chat
Menci Aug 16, 2026
d7b21aa
fix(gateway): let a family say what its fork owns
Menci Aug 16, 2026
6cd92d4
feat(gateway): settle once per run, above the fork
Menci Aug 16, 2026
ff89247
fix(gateway): carry a selector, not the candidate
Menci Aug 16, 2026
3eebd5d
fix(gateway): compose settlement into every family
Menci Aug 16, 2026
018484e
fix(gateway): claim the upstream body, so the run can release it
Menci Aug 16, 2026
082c195
fix(rerank,embeddings): serve a refusal with its own status
Menci Aug 16, 2026
d6946d0
fix(gateway): preserve each family's refusal sentence and complete an…
Menci Aug 16, 2026
d5130e8
feat(gateway): forward upstream response headers through every family…
Menci Aug 16, 2026
d7fd0a4
fix(gateway): give a run a log sink so an unrecorded run still reports
Menci Aug 16, 2026
bbaa158
feat(gateway): make a dial failure and an unreadable answer values, n…
Menci Aug 16, 2026
076132a
feat(images): serve both images endpoints through the pipeline
Menci Aug 16, 2026
e63caf4
fix(gateway): order the pipelined handlers' imports the way the linte…
Menci Aug 16, 2026
c0da795
feat(gateway): answer in the client's own error shape and keep readin…
Menci Aug 16, 2026
36ceb37
test(gateway): assert the failure behaviour the pipeline actually rules
Menci Aug 16, 2026
35f4aed
fix(gateway): read the request before deciding what kind of run it opens
Menci Aug 16, 2026
c0f8259
feat(gateway): answer with a stream, and settle what it billed once i…
Menci Aug 16, 2026
6f66f20
fix(gateway): filter the upstream's headers on the two streaming edges
Menci Aug 16, 2026
bc8e3c2
fix(gateway): drain a streaming answer only after the client has read it
Menci Aug 16, 2026
7d86dcd
fix(audio): stop reading a transcription stream at its terminal event
Menci Aug 16, 2026
4dff883
fix(gateway): catch every dial, and close the dump on a streaming answer
Menci Aug 16, 2026
1a0be81
revert(images): keep both image endpoints on their existing surface
Menci Aug 16, 2026
50a41b0
fix(gateway): answer a refusal in the upstream's own words on the las…
Menci Aug 16, 2026
3a28568
fix(alpha-search): settle a run that searched locally
Menci Aug 16, 2026
ee03cdd
test: exercise the audio family, and assert the log fields the dump c…
Menci Aug 16, 2026
5817f81
fix(gateway): type the forwarded header pairs as tuples
Menci Aug 16, 2026
e6031f1
feat(pipeline): make what a run started a fact it waits for
Menci Aug 16, 2026
8d47f57
feat(audio): carry a transcription's document instead of writing it a…
Menci Aug 16, 2026
6355a3b
feat(protocols): give the images protocol its stream contract
Menci Aug 16, 2026
b36f970
feat(images): carry a streamed answer as facts, and serve both endpoi…
Menci Aug 16, 2026
f5ae077
feat(audio): serve /v1/audio/transcriptions through the pipeline
Menci Aug 16, 2026
038463a
test(images): assert a completed stream closes the read behind it
Menci Aug 16, 2026
c1bc792
feat(gateway): give a dump record a shape, and keep one metadata unde…
Menci Aug 16, 2026
a8966de
docs(gateway): record why a truncated stream still settles as a success
Menci Aug 16, 2026
8ceee12
feat(gateway): give a run's event stream somewhere to be written
Menci Aug 16, 2026
edddcf3
docs(audio): tie the family's own epilogue to the seam's open decision
Menci Aug 16, 2026
4ff5ed2
feat(web): read a dump record of either shape
Menci Aug 16, 2026
1d0cb8b
fix(gateway): record a stream by how it ended, not by the fact that i…
Menci Aug 16, 2026
7e0e113
test(gateway): serve both record shapes through the control plane
Menci Aug 16, 2026
940673f
refactor(gateway): state the dump's error precedence once
Menci Aug 16, 2026
e91863a
feat(gateway): record a pipelined turn as the run it was
Menci Aug 16, 2026
0729e48
refactor(gateway): delete the passthrough surface
Menci Aug 16, 2026
4fb3359
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 16, 2026
60a1941
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 16, 2026
b509941
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 17, 2026
326e2dd
fix(gateway): say where a resource is, not what the host marks
Menci Aug 17, 2026
018a9c6
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 18, 2026
3fc19e4
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 18, 2026
8f84ab7
refactor: spell the five non-chat protocol names in full in the pipel…
Menci Aug 18, 2026
b2ba45f
fix(pipeline): record the run that threw, instead of losing it
Menci Aug 18, 2026
be228b9
feat(dump): give a stream an id, a terminator, and a fact that points…
Menci Aug 18, 2026
aaa961d
feat(pipeline): declare the streamed reading as work the run started
Menci Aug 19, 2026
f611a7d
feat(search): serve /alpha/search through the pipeline written for it
Menci Aug 19, 2026
ee12d17
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
845093d
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
64523d1
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
047877d
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
74aa764
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
77d5c3f
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
4905f52
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
b850068
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
f83091c
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
d174ff0
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
b75c0c0
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 19, 2026
89fe580
refactor(gateway): say why, not what the code used to be
Menci Aug 19, 2026
4e20314
Merge branch 'pr1-core' into feat/pipeline-non-chat
Menci Aug 20, 2026
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
80 changes: 80 additions & 0 deletions apps/web/__tests__/components/requests/detail_test.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions apps/web/__tests__/components/requests/run-render_test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 45 additions & 4 deletions apps/web/src/components/requests/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { PropsWithChildren, ReactNode } from 'react';
import { contentTypeOf, EMPTY_BODY, renderBody, type RenderedBody } from './body-render';
import { errorLabel, requestSeverity } from './format';
import { isSensitiveHeader, redactHeaderValue } from './header-redact';
import { renderRunEvents } from './run-render';
import {
detectCollectKind,
renderStreamEvents,
Expand Down Expand Up @@ -194,14 +195,22 @@ export function RequestDetailPanel({ collected: loadedCollected, error: loadedEr
}
const { collected, error, record, recordId } = shown;

const requestBody = record ? renderBody(record.request.body, contentTypeOf(record.request.headers)) : EMPTY_BODY;
const responseBody = record?.response.body.type === 'bytes' ? renderBody(record.response.body.body, contentTypeOf(record.response.headers)) : EMPTY_BODY;
// The two shapes a record comes in. An endpoint served by the onion is
// recorded as its edges and draws the four sections below; a pipelined one is
// recorded as its whole run, and the run's event stream is the whole of it.
const edge = record?.shape === 'edge' ? record : null;
const requestBody = edge ? renderBody(edge.request.body, contentTypeOf(edge.request.headers)) : EMPTY_BODY;
const responseBody = edge?.response.body.type === 'bytes' ? renderBody(edge.response.body.body, contentTypeOf(edge.response.headers)) : EMPTY_BODY;
const streamEvents = useMemo<DumpStreamEvent[]>(
() => record?.response.body.type === 'stream' ? record.response.body.events : [],
() => record?.shape === 'edge' && record.response.body.type === 'stream' ? record.response.body.events : [],
[record],
);
const collectKind = record ? detectCollectKind(record.meta.path) : null;
const renderedEvents = useMemo(() => renderStreamEvents(collectKind, streamEvents), [collectKind, streamEvents]);
const runEvents = useMemo(
() => record?.shape === 'run' ? renderRunEvents(record.events) : [],
[record],
);

if (!recordId) return <div className="grid h-full place-items-center p-4"><EmptyStateLine>{t('dashboard.requests.selectPrompt')}</EmptyStateLine></div>;
// This replaces every section rather than sitting in one, so it takes the
Expand All @@ -210,8 +219,40 @@ export function RequestDetailPanel({ collected: loadedCollected, error: loadedEr
if (error) return <OutcomeMessageBar className="!m-4">{error}</OutcomeMessageBar>;
if (!record) return null;

const severity = requestSeverity(record.response.status, record.meta.error);
const severity = requestSeverity(record.meta.status, record.meta.error);
const responseError = errorLabel(record.meta.error);

if (record.shape === 'run') {
return (
<ScrollArea axes="vertical" className="h-full" contentClassName="min-h-full" noTabIndex>
<section>
<DetailSectionHeader
title={t('dashboard.requests.run')}
detail={<>
<HttpMethodBadge method={record.meta.method} />
<Text size={300} className="font-mono">{record.meta.path}</Text>
<HttpStatusBadge severity={severity}>{record.meta.status ?? t('dashboard.requests.noStatus')}</HttpStatusBadge>
{responseError && <Text size={200} className={dangerText}>{responseError}</Text>}
</>}
copyText={record.events || undefined}
/>
<SectionBody>
{runEvents.length === 0 ? <EmptyStateLine className="p-4">{t('dashboard.requests.noRunEvents')}</EmptyStateLine> : runEvents.map((event, index) => (
<div className={s.section} key={index}>
<div className="flex items-center gap-2 px-4 pt-3">
<Text size={100} className="font-mono mono-size-100 text-fui-fg2">{event.type || t('dashboard.requests.unlabeled')}</Text>
{event.subject && <Text size={100} className="font-mono mono-size-100 text-fui-fg3">{event.subject}</Text>}
{event.parseError && <Text size={100} className={dangerText}>{t('dashboard.requests.jsonParseFailed')}</Text>}
</div>
<CodeView body={{ text: event.text, copyText: event.text, decodeError: event.parseError, isJson: !event.parseError }} />
</div>
))}
</SectionBody>
</section>
</ScrollArea>
);
}

const requestHeadersCopy = record.request.headers.map(([name, value]) => `${name}: ${value}`).join('\n');
const responseHeadersCopy = record.response.headers.map(([name, value]) => `${name}: ${value}`).join('\n');
const collectedCopyText = collected?.result === null || collected?.result === undefined
Expand Down
46 changes: 46 additions & 0 deletions apps/web/src/components/requests/run-render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { errorMessage } from '../../lib/error-message';
import type { DumpEvent } from '@floway-dev/gateway/dump-types';

// A pipelined turn is recorded as its whole run: one NDJSON line per event, in
// the order the run emitted them. A line is also one SSE `data:` payload, so
// what this reads out of a stored record is what a live observer would be handed
// frame by frame.

export interface RenderedRunEvent {
/** The event's own kind — `stage.entered`, `object`, `stream.frame`, … */
type: string;
/** Which stage, object or stream it is about. Ids, not prose. */
subject: string | null;
text: string;
parseError: string | null;
}

// Only three of the six name a stage, and each of the other three names its own
// namespace, so the subject is read off the event rather than looked up.
const subjectOf = (event: DumpEvent): string | null => {
switch (event.type) {
case 'stage.entered': return event.name;
case 'stage.leaved': return `#${event.stageId}`;
case 'stage.log': return event.level;
case 'object': return `#${event.fromObjectId}`;
case 'stream.frame':
case 'stream.end': return `#${event.streamId}`;
}
};

// A line the gateway wrote and this cannot parse is shown as it was stored, with
// the failure named: a record that renders as nothing would read as an empty run.
export const renderRunEvents = (ndjson: string): RenderedRunEvent[] =>
ndjson.split('\n').filter(line => line.length > 0).map(line => {
try {
const event = JSON.parse(line) as DumpEvent;
return {
type: event.type,
subject: subjectOf(event),
text: JSON.stringify(event, null, 2),
parseError: null,
};
} catch (error) {
return { type: '', subject: null, text: line, parseError: errorMessage(error) };
}
});
2 changes: 2 additions & 0 deletions apps/web/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,8 @@ const en = {
apiKeysLink: 'API Keys',
request: 'Request',
requestBody: 'Request body',
run: 'Run',
noRunEvents: 'This run recorded no events.',
response: 'Response',
responseBody: 'Response body',
noRequestBody: 'No request body',
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/i18n/locales/zh-Hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,8 @@ const zhHansCN = {
apiKeysLink: 'API 密钥',
request: '请求',
requestBody: '请求体',
run: '运行过程',
noRunEvents: '这次运行没有记录到任何事件。',
response: '响应',
responseBody: '响应体',
noRequestBody: '没有请求体',
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/routes/dashboard-monitor-requests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs): Promise
]);
const record = recordResult?.data ?? null;
const collectKind = record ? detectCollectKind(record.meta.path) : null;
const streamEvents = record?.response.body.type === 'stream' ? record.response.body.events : [];
// Only an edge-shaped record carries a captured frame log to collect; a run
// records its stream inside its own events and the detail panel reads it there.
const streamEvents = record?.shape === 'edge' && record.response.body.type === 'stream' ? record.response.body.events : [];
const collected = collectKind && streamEvents.length ? await collectStream(collectKind, streamEvents) : null;
return {
collected,
Expand Down
37 changes: 36 additions & 1 deletion packages/gateway/__tests__/control-plane/dump_test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading