-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathllm-providers.helpers.ts
More file actions
192 lines (177 loc) 路 6.05 KB
/
Copy pathllm-providers.helpers.ts
File metadata and controls
192 lines (177 loc) 路 6.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import { isProviderType } from '@/lib/llm-providers/providerTemplates'
import type { LlmProviderConfig } from '@/lib/llm-providers/types'
/**
* A provider row as the server returns it.
*
* Absent values are null rather than undefined, and credentials are not here
* at all: the server reports only whether each is set, so a key cannot reach
* a surface that has no use for it.
*/
export interface ProviderRow {
id: string
kind: 'llm' | 'acp'
type: string
name: string
baseUrl: string | null
// Nullable since the table holds coding agents too, and those carry neither.
modelId: string | null
supportsImages: boolean
contextWindow: number | null
temperature: number
hasApiKey: boolean
hasAccessKeyId: boolean
hasSecretAccessKey: boolean
hasSessionToken: boolean
resourceName: string | null
region: string | null
reasoningEffort: string | null
reasoningSummary: string | null
createdAt: number
updatedAt: number
}
function orUndefined<T>(value: T | null): T | undefined {
return value ?? undefined
}
function toReasoningSummary(
value: string | null,
): LlmProviderConfig['reasoningSummary'] {
if (value === 'auto' || value === 'concise' || value === 'detailed') {
return value
}
return undefined
}
/**
* Converts a stored row to the config shape the app works in.
*
* Returns null for a type this build does not know, which happens after a
* downgrade from a build that added one. The row stays in the database and
* reappears on upgrade; showing it would push an unknown key through the icon
* map, the template lookup and the default base URLs, all keyed by the union.
*/
export function toProviderConfig(row: ProviderRow): LlmProviderConfig | null {
// Coding agents share this table and this endpoint, and are served to the
// surfaces that want them through their own hook. Filtering on kind says
// that; leaning on the unknown-type guard below to drop them happened to
// work and said something else entirely.
if (row.kind !== 'llm') return null
if (!isProviderType(row.type)) return null
if (row.modelId === null || row.contextWindow === null) return null
return {
id: row.id,
type: row.type,
name: row.name,
baseUrl: orUndefined(row.baseUrl),
modelId: row.modelId,
supportsImages: row.supportsImages,
contextWindow: row.contextWindow,
temperature: row.temperature,
hasApiKey: row.hasApiKey,
hasAccessKeyId: row.hasAccessKeyId,
hasSecretAccessKey: row.hasSecretAccessKey,
hasSessionToken: row.hasSessionToken,
resourceName: orUndefined(row.resourceName),
region: orUndefined(row.region),
reasoningEffort: orUndefined(row.reasoningEffort),
reasoningSummary: toReasoningSummary(row.reasoningSummary),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
}
}
export function toProviderConfigs(rows: readonly ProviderRow[]) {
return rows
.map(toProviderConfig)
.filter((config): config is LlmProviderConfig => config !== null)
}
/** The request body for a provider write. `id` travels in the path instead. */
export function toProviderPayload(config: LlmProviderConfig) {
return {
type: config.type,
name: config.name,
baseUrl: config.baseUrl,
modelId: config.modelId,
supportsImages: config.supportsImages,
contextWindow: config.contextWindow,
temperature: config.temperature,
apiKey: config.apiKey,
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
sessionToken: config.sessionToken,
resourceName: config.resourceName,
region: config.region,
reasoningEffort: config.reasoningEffort,
reasoningSummary: config.reasoningSummary,
createdAt: config.createdAt,
}
}
/**
* Provider types where a second copy makes no sense, because the credential is
* an OAuth grant held once per account rather than a key the user can hold
* several of.
*/
const SINGLE_INSTANCE_PROVIDER_TYPES = new Set<LlmProviderConfig['type']>([
'chatgpt-pro',
'github-copilot',
'qwen-code',
])
export interface ProviderSavePlan {
saved: LlmProviderConfig
removedIds: string[]
}
/**
* Works out the writes a save turns into.
*
* Extension storage took the whole list at once, so collapsing an earlier copy
* of a single-instance provider fell out of replacing the array. Over HTTP the
* save is one PUT, so the copies it displaces have to be deleted explicitly,
* and the surviving id has to be the earlier one so the row keeps its
* identity rather than accumulating a new one per sign-in.
*/
export function planProviderSave(
current: readonly LlmProviderConfig[],
provider: LlmProviderConfig,
now = Date.now(),
): ProviderSavePlan {
if (!SINGLE_INSTANCE_PROVIDER_TYPES.has(provider.type)) {
const existing = current.find((candidate) => candidate.id === provider.id)
return {
saved: existing
? { ...provider, updatedAt: now }
: { ...provider, createdAt: now, updatedAt: now },
removedIds: [],
}
}
const existing =
current.find((candidate) => candidate.id === provider.id) ??
current.find((candidate) => candidate.type === provider.type)
const saved: LlmProviderConfig = {
...provider,
id: existing?.id ?? provider.id,
createdAt: existing?.createdAt ?? now,
updatedAt: now,
}
const removedIds = current
.filter(
(candidate) =>
candidate.id !== saved.id &&
(candidate.type === provider.type || candidate.id === provider.id),
)
.map((candidate) => candidate.id)
return { saved, removedIds }
}
/**
* Ids present before a save but not after.
*
* Saving a single-instance provider (an OAuth one, where a second copy makes
* no sense) collapses any earlier copy into the saved one. In extension
* storage that fell out of writing the whole list at once; over HTTP the
* removals have to be issued explicitly.
*/
export function removedProviderIds(
before: readonly LlmProviderConfig[],
after: readonly LlmProviderConfig[],
): string[] {
const kept = new Set(after.map((provider) => provider.id))
return before
.filter((provider) => !kept.has(provider.id))
.map((provider) => provider.id)
}