dsh-code 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +4 -2
- package/README.md +4 -2
- package/bin/deepseek.mjs +38 -3
- package/lib/index.mjs +1485 -906
- package/lib/startup.mjs +21 -9
- package/lib/theme-BEi4i_aN.mjs +624 -0
- package/lib/types/app.d.ts +31 -3
- package/lib/types/index.d.ts +1 -0
- package/lib/types/kernel-panels.d.ts +21 -0
- package/lib/types/mentions.d.ts +29 -12
- package/lib/types/models.d.ts +66 -0
- package/lib/types/render/animations.d.ts +175 -2
- package/lib/types/render/projection.d.ts +38 -7
- package/lib/types/render/status.d.ts +34 -13
- package/lib/types/startup.d.ts +12 -4
- package/lib/types/theme-panel.d.ts +24 -0
- package/lib/types/theme.d.ts +158 -2
- package/package.json +1 -1
- package/src/app.ts +510 -130
- package/src/index.ts +964 -900
- package/src/kernel-panels.ts +481 -419
- package/src/mentions.ts +57 -27
- package/src/models.ts +200 -66
- package/src/render/animations.ts +359 -2
- package/src/render/projection.ts +764 -655
- package/src/render/status.ts +744 -603
- package/src/startup.ts +119 -109
- package/src/theme-panel.ts +72 -0
- package/src/theme.ts +206 -70
package/src/mentions.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workspace @mention support: file candidates from a bounded
|
|
3
|
-
* the session cwd, session candidates from the opt-in
|
|
4
|
-
* service, and submission preparation through its
|
|
5
|
-
* session mentions land as canonical
|
|
6
|
-
* submit the text is parsed back into
|
|
7
|
-
* references, snapshots are injected
|
|
8
|
-
*
|
|
9
|
-
* upstream README's wiring.
|
|
2
|
+
* Workspace @mention support: file and directory candidates from a bounded
|
|
3
|
+
* async scan of the session cwd, session candidates from the opt-in
|
|
4
|
+
* `sessionReferenceResolver` service, and submission preparation through its
|
|
5
|
+
* `prepare()` API. Picked session mentions land as canonical
|
|
6
|
+
* `@[label](dsh-session:…)` tokens; on submit the text is parsed back into
|
|
7
|
+
* readable `@label` text plus structured references, snapshots are injected
|
|
8
|
+
* via `agent.inject()` before the readable message wakes the driver
|
|
9
|
+
* (`followup` idle, `steer` running) — exactly the upstream README's wiring.
|
|
10
|
+
*
|
|
11
|
+
* Harness exposes no workspace-file mention service (only session references
|
|
12
|
+
* plus a post-hoc produced-file linker), so the file index is the lightweight
|
|
13
|
+
* bounded scan below, kept deliberately smaller than Codex's streaming
|
|
14
|
+
* gitignore-aware walker.
|
|
10
15
|
*
|
|
11
16
|
* @module @deepseek-ai/dsh-code/mentions
|
|
12
17
|
*/
|
|
@@ -57,8 +62,17 @@ export interface PreparedMention {
|
|
|
57
62
|
const SKIP_DIRS = new Set(['.git', 'node_modules', 'lib', 'dist', 'out', '.omc', 'coverage'])
|
|
58
63
|
const MAX_FILES = 4000
|
|
59
64
|
const MAX_DEPTH = 12
|
|
65
|
+
/** Empty-query default rows: proves the index exists without typing (Codex's
|
|
66
|
+
* popups show something on a bare sigil too). */
|
|
67
|
+
const EMPTY_QUERY_ROWS = 20
|
|
60
68
|
|
|
61
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Bounded async BFS scan of a workspace; unreadable entries are skipped.
|
|
71
|
+
* Both files and directories are indexed (directories insert with a trailing
|
|
72
|
+
* slash), mirroring Codex's `MatchType::{File,Directory}` index. Dotfiles and
|
|
73
|
+
* the {@link SKIP_DIRS} list are excluded, which is a coarser filter than
|
|
74
|
+
* Codex's gitignore-aware walker but stays dependency-free and bounded.
|
|
75
|
+
*/
|
|
62
76
|
export async function scanWorkspaceFiles(root: string, signal?: AbortSignal): Promise<readonly FileCandidate[]> {
|
|
63
77
|
const found: FileCandidate[] = []
|
|
64
78
|
const pending: Array<{ absolute: string; relative: string; depth: number }> = [{ absolute: root, relative: '', depth: 0 }]
|
|
@@ -78,6 +92,7 @@ export async function scanWorkspaceFiles(root: string, signal?: AbortSignal): Pr
|
|
|
78
92
|
const relative = current.relative === '' ? entry.name : `${current.relative}/${entry.name}`
|
|
79
93
|
if (entry.isDirectory()) {
|
|
80
94
|
if (SKIP_DIRS.has(entry.name) || current.depth + 1 > MAX_DEPTH) continue
|
|
95
|
+
found.push({ path: relative, kind: 'directory' })
|
|
81
96
|
pending.push({ absolute: join(current.absolute, entry.name), relative, depth: current.depth + 1 })
|
|
82
97
|
} else if (entry.isFile()) {
|
|
83
98
|
found.push({ path: relative, kind: 'file' })
|
|
@@ -111,7 +126,7 @@ function scoreFile(path: string, query: string): number {
|
|
|
111
126
|
|
|
112
127
|
/** The mention API the input editor and the runner share. */
|
|
113
128
|
export interface MentionsApi {
|
|
114
|
-
/** Scanned workspace files, cached across one session. */
|
|
129
|
+
/** Scanned workspace files and directories, cached across one session. */
|
|
115
130
|
files(): Promise<readonly FileCandidate[]>
|
|
116
131
|
/** Ranked menu candidates for the typed `@` query. */
|
|
117
132
|
candidates(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]>
|
|
@@ -129,49 +144,64 @@ export interface MentionsApi {
|
|
|
129
144
|
/**
|
|
130
145
|
* Create the mention API for one agent's workspace. A missing
|
|
131
146
|
* session-reference service degrades to file mentions only (the scan still
|
|
132
|
-
* works); `prepare` then passes text through untouched.
|
|
147
|
+
* works); `prepare` then passes text through untouched. An undefined agent
|
|
148
|
+
* (a bare launch before any session exists) also degrades to file-only
|
|
149
|
+
* mentions, so `@` file completion works before the first message.
|
|
150
|
+
*
|
|
151
|
+
* `files` is hoisted into the closure so `candidates` never reaches for
|
|
152
|
+
* `this` — the runner hands `mentions.candidates` to the input editor as a
|
|
153
|
+
* detached callback, and a `this`-bound method would throw on every `@` key.
|
|
133
154
|
* @param ctx - context carrying the optional `sessionReferenceResolver`.
|
|
134
155
|
* @param agent - the session owner; excluded from its own candidates.
|
|
135
156
|
* @param cwd - workspace root to scan.
|
|
136
157
|
*/
|
|
137
|
-
export function createMentions(ctx: Context, agent: Agent, cwd: string): MentionsApi {
|
|
158
|
+
export function createMentions(ctx: Context, agent: Agent | undefined, cwd: string): MentionsApi {
|
|
138
159
|
const resolver = ctx.get('sessionReferenceResolver')
|
|
160
|
+
const sessionCapable = agent !== undefined && resolver !== undefined
|
|
139
161
|
let filesPromise: Promise<readonly FileCandidate[]> | undefined
|
|
162
|
+
const files = (): Promise<readonly FileCandidate[]> => {
|
|
163
|
+
filesPromise ??= scanWorkspaceFiles(cwd)
|
|
164
|
+
return filesPromise
|
|
165
|
+
}
|
|
140
166
|
|
|
141
167
|
return {
|
|
142
|
-
files
|
|
143
|
-
filesPromise ??= scanWorkspaceFiles(cwd)
|
|
144
|
-
return filesPromise
|
|
145
|
-
},
|
|
168
|
+
files,
|
|
146
169
|
async candidates(query: string, signal?: AbortSignal): Promise<readonly MentionCandidate[]> {
|
|
147
170
|
const needle = query.trim()
|
|
148
|
-
const [
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
?
|
|
152
|
-
:
|
|
171
|
+
const [scanned, sessions] = await Promise.all([
|
|
172
|
+
files(),
|
|
173
|
+
sessionCapable && needle !== '' && agent !== undefined
|
|
174
|
+
? resolver!.listCandidates(agent, needle, 10, signal).catch(() => [] as readonly SessionReferenceCandidate[])
|
|
175
|
+
: Promise.resolve([] as readonly SessionReferenceCandidate[]),
|
|
153
176
|
])
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.slice(0,
|
|
177
|
+
// A bare `@` lists the first path-sorted entries so the menu is live
|
|
178
|
+
// before any typing; a non-empty needle ranks by the Codex-shaped score.
|
|
179
|
+
const fileRows: MentionCandidate[] = (needle === ''
|
|
180
|
+
? scanned.slice(0, EMPTY_QUERY_ROWS)
|
|
181
|
+
: scanned
|
|
182
|
+
.filter(candidate => scoreFile(candidate.path, needle) > 0)
|
|
183
|
+
.sort((left, right) => scoreFile(right.path, needle) - scoreFile(left.path, needle))
|
|
184
|
+
.slice(0, 20))
|
|
158
185
|
.map(candidate => ({
|
|
159
186
|
label: candidate.path,
|
|
160
187
|
description: candidate.kind === 'directory' ? 'Folder' : 'File',
|
|
161
188
|
kind: candidate.kind,
|
|
162
189
|
}))
|
|
190
|
+
// Sessions join only with a typed needle and always AFTER the file
|
|
191
|
+
// rows: `@` is a file mention first (Codex's semantics), and session
|
|
192
|
+
// references are the secondary vocabulary.
|
|
163
193
|
const sessionRows: MentionCandidate[] = sessions.map(candidate => ({
|
|
164
194
|
label: formatSessionReferenceMention(candidate),
|
|
165
195
|
description: `Session · ${candidate.cwd ?? '(no cwd)'}`,
|
|
166
196
|
kind: 'session',
|
|
167
197
|
}))
|
|
168
|
-
return [...
|
|
198
|
+
return [...fileRows, ...sessionRows]
|
|
169
199
|
},
|
|
170
200
|
parse(text: string): ParsedSessionReferenceText {
|
|
171
201
|
return parseSessionReferenceText(text)
|
|
172
202
|
},
|
|
173
203
|
async prepare(parsed: ParsedSessionReferenceText, signal?: AbortSignal): Promise<PreparedMention> {
|
|
174
|
-
if (parsed.references.length === 0 || resolver === undefined) {
|
|
204
|
+
if (parsed.references.length === 0 || resolver === undefined || agent === undefined) {
|
|
175
205
|
return { text: parsed.text, references: parsed.references }
|
|
176
206
|
}
|
|
177
207
|
const prepared = await resolver.prepare(
|
package/src/models.ts
CHANGED
|
@@ -1,66 +1,200 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model directory for the `/model` panel: the in-process equivalent of the
|
|
3
|
-
* web host's model catalog (`buildModelCatalog`), reading the advisory
|
|
4
|
-
* `ctx.llm` registry directly. Catalog membership is advisory — a route
|
|
5
|
-
* serving a model it stopped advertising stays usable — so selection never
|
|
6
|
-
* fails on catalog absence alone.
|
|
7
|
-
*
|
|
8
|
-
* @module @deepseek-ai/dsh-tui/models
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
-
import type {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Model directory for the `/model` panel: the in-process equivalent of the
|
|
3
|
+
* web host's model catalog (`buildModelCatalog`), reading the advisory
|
|
4
|
+
* `ctx.llm` registry directly. Catalog membership is advisory — a route
|
|
5
|
+
* serving a model it stopped advertising stays usable — so selection never
|
|
6
|
+
* fails on catalog absence alone.
|
|
7
|
+
*
|
|
8
|
+
* @module @deepseek-ai/dsh-tui/models
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
import type { ModelSelection } from '@deepseek-ai/dsh-agent'
|
|
13
|
+
import {
|
|
14
|
+
ReasoningEffortId,
|
|
15
|
+
type LlmModelInfo,
|
|
16
|
+
type LlmModelReasoningInfo,
|
|
17
|
+
type LlmResolvedModelInfo,
|
|
18
|
+
} from '@deepseek-ai/dsh-llm'
|
|
19
|
+
|
|
20
|
+
/** Display metadata for one adapter-owned reasoning effort (mirrors `LlmReasoningEffortInfo`). */
|
|
21
|
+
export interface ModelReasoningEffort {
|
|
22
|
+
/** Opaque value accepted by the model's `GenerateOptions.reasoningEffort`. */
|
|
23
|
+
id: string
|
|
24
|
+
/** Human-readable effort name for selectors. */
|
|
25
|
+
name: string
|
|
26
|
+
/** Optional user-facing distinction from otherwise similar efforts. */
|
|
27
|
+
description?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Selectable reasoning efforts for one model (mirrors `LlmModelReasoningInfo`). */
|
|
31
|
+
export interface ModelReasoning {
|
|
32
|
+
/** Supported efforts in adapter-preferred display order. */
|
|
33
|
+
efforts: readonly ModelReasoningEffort[]
|
|
34
|
+
/** Adapter-configured default materialized when callers omit an effort. */
|
|
35
|
+
defaultEffort?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One selectable row in the `/model` panel. */
|
|
39
|
+
export interface ModelRow {
|
|
40
|
+
/** Registered provider route. */
|
|
41
|
+
provider: string
|
|
42
|
+
/** Display name of the provider route. */
|
|
43
|
+
providerName: string
|
|
44
|
+
/** Provider-owned model id. */
|
|
45
|
+
model: string
|
|
46
|
+
/** Human-readable model name. */
|
|
47
|
+
modelName: string
|
|
48
|
+
/** Adapter-owned selectable reasoning levels when the model exposes any. */
|
|
49
|
+
reasoning?: ModelReasoning
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The resolved directory: rows plus per-provider discovery failures. */
|
|
53
|
+
export interface ModelDirectory {
|
|
54
|
+
/** Advisory rows, provider-major in registry order. */
|
|
55
|
+
rows: readonly ModelRow[]
|
|
56
|
+
/** Provider ids whose model listing failed; those providers contribute no rows. */
|
|
57
|
+
failures: readonly string[]
|
|
58
|
+
/**
|
|
59
|
+
* `provider/model` labels whose per-model capability lookup failed. Those
|
|
60
|
+
* rows still appear (advisory degrade, mirroring the web catalog), but
|
|
61
|
+
* without an effort picker — a picker caller must not misread the absence
|
|
62
|
+
* as "this model exposes no reasoning" (e.g. deepseek-v4-flash always
|
|
63
|
+
* advertises off/high/max unless thinking is disabled). Optional for
|
|
64
|
+
* callers that shape a directory by hand; {@link loadModelDirectory}
|
|
65
|
+
* always populates it (empty when nothing failed).
|
|
66
|
+
*/
|
|
67
|
+
reasoningFailures?: readonly string[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Map an adapter's reasoning capability onto the panel's plain-id shape. */
|
|
71
|
+
export function mapReasoning(reasoning: LlmModelReasoningInfo): ModelReasoning {
|
|
72
|
+
return {
|
|
73
|
+
efforts: reasoning.efforts.map(effort => ({
|
|
74
|
+
id: effort.id,
|
|
75
|
+
name: effort.name,
|
|
76
|
+
...effort.description === undefined ? {} : { description: effort.description },
|
|
77
|
+
})),
|
|
78
|
+
...reasoning.defaultEffort === undefined ? {} : { defaultEffort: reasoning.defaultEffort },
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the effective model selection for one live session, in the
|
|
84
|
+
* documented precedence: the in-process explicit pick, then the session's
|
|
85
|
+
* last `request/header` config, then the deployment default.
|
|
86
|
+
* @param picked - the explicit selection made in this process, when any.
|
|
87
|
+
* @param logged - the last logged request header config, when any.
|
|
88
|
+
* @param defaults - the deployment default selection.
|
|
89
|
+
* @returns the effective selection, carrying a reasoning effort when one is in force.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveEffectiveSelection(
|
|
92
|
+
picked: ModelSelection | undefined,
|
|
93
|
+
logged: { provider: string; model: string; reasoningEffort?: string } | undefined,
|
|
94
|
+
defaults: ModelSelection,
|
|
95
|
+
): ModelSelection {
|
|
96
|
+
if (picked !== undefined) return picked
|
|
97
|
+
if (logged !== undefined) {
|
|
98
|
+
return {
|
|
99
|
+
provider: logged.provider,
|
|
100
|
+
model: logged.model,
|
|
101
|
+
...logged.reasoningEffort === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { reasoningEffort: ReasoningEffortId(logged.reasoningEffort) },
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return defaults
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the selection one `/model` pick applies, rejecting an effort the
|
|
111
|
+
* row does not advertise. The row is the picker's source of truth, so a
|
|
112
|
+
* stale directory cannot smuggle an unsupported effort into the next step
|
|
113
|
+
* (the request pipeline would reject it before network I/O regardless). The
|
|
114
|
+
* empty string is the picker's "provider default" sentinel — an explicit
|
|
115
|
+
* choice to leave the effort to the model's own default, exactly like an
|
|
116
|
+
* absent effort.
|
|
117
|
+
* @param row - the picked model row.
|
|
118
|
+
* @param effortId - the chosen advertised effort, '' or undefined for the model default.
|
|
119
|
+
* @returns the selection the runner records for the next assembled step.
|
|
120
|
+
*/
|
|
121
|
+
export function buildModelSelection(row: ModelRow, effortId?: string): ModelSelection {
|
|
122
|
+
const selected = effortId === undefined || effortId === '' ? undefined : effortId
|
|
123
|
+
if (selected !== undefined
|
|
124
|
+
&& (row.reasoning === undefined || !row.reasoning.efforts.some(effort => effort.id === selected))) {
|
|
125
|
+
throw new Error(`model ${row.provider}/${row.model} does not support reasoning effort "${selected}"`)
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
provider: row.provider,
|
|
129
|
+
model: row.model,
|
|
130
|
+
...selected === undefined ? {} : { reasoningEffort: ReasoningEffortId(selected) },
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Display label for one applied selection: `provider/model` or `provider/model@effort`. */
|
|
135
|
+
export function modelSelectionLabel(selection: ModelSelection): string {
|
|
136
|
+
return selection.reasoningEffort === undefined
|
|
137
|
+
? `${selection.provider}/${selection.model}`
|
|
138
|
+
: `${selection.provider}/${selection.model}@${selection.reasoningEffort}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Load the selectable model directory from the live `ctx.llm` registry.
|
|
143
|
+
* Providers are listed synchronously; each provider's models are discovered
|
|
144
|
+
* with a bounded parallel fan-out whose failures degrade to that provider
|
|
145
|
+
* contributing no rows (mirrors the web catalog's per-provider failures).
|
|
146
|
+
* Each row's reasoning levels are resolved per exact model like the web
|
|
147
|
+
* catalog (`buildModelCatalog`); a single model's capability lookup failure
|
|
148
|
+
* degrades to that row having no effort picker rather than hiding the model,
|
|
149
|
+
* and the failure rides `reasoningFailures` so the caller can tell "no
|
|
150
|
+
* advertised reasoning" from "capability lookup failed".
|
|
151
|
+
* @param ctx - context carrying the `llm` service.
|
|
152
|
+
* @returns the resolved directory; empty rows when `llm` is unavailable.
|
|
153
|
+
*/
|
|
154
|
+
export async function loadModelDirectory(ctx: Context): Promise<ModelDirectory> {
|
|
155
|
+
const llm = ctx.get('llm')
|
|
156
|
+
if (llm === undefined) return { rows: [], failures: [], reasoningFailures: [] }
|
|
157
|
+
// Call resolveModelInfo AS A METHOD (llm.resolveModelInfo(...)): destructured
|
|
158
|
+
// off the service it loses `this` — this.registration() then throws on the
|
|
159
|
+
// first provider, the catch swallows it, and every row lands in
|
|
160
|
+
// reasoningFailures ("capability lookup failed") even though the adapter
|
|
161
|
+
// itself never fails.
|
|
162
|
+
const llmResolve = llm as { resolveModelInfo?: (provider: string, model: string) => Promise<LlmResolvedModelInfo> }
|
|
163
|
+
const providers = llm.listProviders()
|
|
164
|
+
const listed = await Promise.all(providers.map(async (provider) => {
|
|
165
|
+
try {
|
|
166
|
+
const models: readonly LlmModelInfo[] = await llm.listModels(provider.id)
|
|
167
|
+
const reasoningFailures: string[] = []
|
|
168
|
+
const rows = await Promise.all(models.map(async (model): Promise<ModelRow> => {
|
|
169
|
+
const row: ModelRow = {
|
|
170
|
+
provider: provider.id,
|
|
171
|
+
providerName: provider.name,
|
|
172
|
+
model: model.id,
|
|
173
|
+
modelName: model.name,
|
|
174
|
+
}
|
|
175
|
+
if (llmResolve.resolveModelInfo === undefined) return row
|
|
176
|
+
try {
|
|
177
|
+
const resolved = await llmResolve.resolveModelInfo(provider.id, model.id)
|
|
178
|
+
return resolved.reasoning === undefined
|
|
179
|
+
? row
|
|
180
|
+
: { ...row, reasoning: mapReasoning(resolved.reasoning) }
|
|
181
|
+
} catch {
|
|
182
|
+
reasoningFailures.push(`${provider.id}/${model.id}`)
|
|
183
|
+
return row
|
|
184
|
+
}
|
|
185
|
+
}))
|
|
186
|
+
return {
|
|
187
|
+
provider: provider.id,
|
|
188
|
+
providerName: provider.name,
|
|
189
|
+
models: rows,
|
|
190
|
+
reasoningFailures,
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
return { provider: provider.id, providerName: provider.name, models: [] as ModelRow[], failed: true }
|
|
194
|
+
}
|
|
195
|
+
}))
|
|
196
|
+
const rows = listed.flatMap(entry => entry.models)
|
|
197
|
+
const failures = listed.filter(entry => 'failed' in entry && entry.failed === true).map(entry => entry.provider)
|
|
198
|
+
const reasoningFailures = listed.flatMap(entry => 'failed' in entry ? [] : entry.reasoningFailures)
|
|
199
|
+
return { rows, failures, reasoningFailures }
|
|
200
|
+
}
|